@vercel/factory 0.0.13 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +307 -1
- package/dist/agent-routes.d.mts +69 -0
- package/dist/agent-routes.mjs +64 -0
- package/dist/agent-routes.mjs.map +1 -0
- package/dist/api-contracts.d.mts +1749 -0
- package/dist/api-contracts.mjs +118 -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 +77 -0
- package/dist/api.mjs +1087 -0
- package/dist/api.mjs.map +1 -0
- 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 +692 -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 +44 -8
- package/dist/blob/index.mjs +159 -35
- package/dist/blob/index.mjs.map +1 -1
- package/dist/budget.d.mts +108 -0
- package/dist/budget.mjs +141 -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 +6 -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 +24 -0
- package/dist/change-verification/dispatch.mjs +45 -0
- package/dist/change-verification/dispatch.mjs.map +1 -0
- package/dist/change-verification/eve-tool.d.mts +60 -0
- package/dist/change-verification/eve-tool.mjs +101 -0
- package/dist/change-verification/eve-tool.mjs.map +1 -0
- package/dist/changes/eve-record-change.d.mts +56 -0
- package/dist/changes/eve-record-change.mjs +97 -0
- package/dist/changes/eve-record-change.mjs.map +1 -0
- package/dist/changes/presentation.d.mts +14 -0
- package/dist/changes.d.mts +23 -0
- package/dist/changes.mjs +29 -0
- package/dist/changes.mjs.map +1 -0
- package/dist/client-events.d.mts +18 -0
- package/dist/client-events.mjs +41 -0
- package/dist/client-events.mjs.map +1 -0
- package/dist/client-stream.d.mts +28 -0
- package/dist/client-stream.mjs +204 -0
- package/dist/client-stream.mjs.map +1 -0
- package/dist/client-transcript.d.mts +41 -0
- package/dist/client-transcript.mjs +229 -0
- package/dist/client-transcript.mjs.map +1 -0
- package/dist/client.d.mts +234 -0
- package/dist/client.mjs +258 -0
- package/dist/client.mjs.map +1 -0
- package/dist/code-review/contracts.d.mts +26 -0
- package/dist/code-review/eve-post-review.d.mts +70 -0
- package/dist/code-review/eve-post-review.mjs +183 -0
- package/dist/code-review/eve-post-review.mjs.map +1 -0
- package/dist/code-review/eve-review-comments.d.mts +64 -0
- package/dist/code-review/eve-review-comments.mjs +159 -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 +44 -0
- package/dist/code-review/github-reporter.mjs +260 -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 +6 -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 -2
- package/dist/connectors/index.mjs +4 -26
- 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 -41
- package/dist/connectors/slack.mjs +13 -69
- package/dist/connectors/slack.mjs.map +1 -1
- package/dist/deepsec/eve-tool.d.mts +110 -0
- package/dist/deepsec/eve-tool.mjs +164 -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 +81 -0
- package/dist/dispatch.mjs +140 -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 +143 -1561
- package/dist/eve/index.mjs +244 -1063
- package/dist/eve/index.mjs.map +1 -1
- package/dist/eve/invoke.d.mts +144 -0
- package/dist/eve/invoke.mjs +241 -0
- package/dist/eve/invoke.mjs.map +1 -0
- package/dist/eve/session-client.d.mts +68 -0
- package/dist/eve/session-client.mjs +131 -0
- package/dist/eve/session-client.mjs.map +1 -0
- package/dist/eve/task-execution.mjs +18 -0
- package/dist/eve/task-execution.mjs.map +1 -0
- package/dist/eve/task-session.d.mts +87 -0
- package/dist/eve/task-session.mjs +203 -0
- package/dist/eve/task-session.mjs.map +1 -0
- package/dist/eve/transcript.d.mts +37 -0
- package/dist/eve/transcript.mjs +65 -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 +75 -0
- package/dist/execution.mjs +153 -0
- package/dist/execution.mjs.map +1 -0
- package/dist/finding-remediation/admission.d.mts +51 -0
- package/dist/finding-remediation/admission.mjs +87 -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 +6 -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 +198 -0
- package/dist/github-publication.mjs +429 -0
- package/dist/github-publication.mjs.map +1 -0
- package/dist/github-transfer.d.mts +72 -0
- package/dist/github-transfer.mjs +125 -0
- package/dist/github-transfer.mjs.map +1 -0
- package/dist/github.d.mts +406 -2
- package/dist/github.mjs +250 -37
- 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/index.d.mts +14 -314
- package/dist/index.mjs +14 -1124
- package/dist/intake-contracts.d.mts +56 -0
- package/dist/intake-contracts.mjs +33 -0
- package/dist/intake-contracts.mjs.map +1 -0
- package/dist/integrations/deepsec.d.mts +4 -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 +3 -0
- package/dist/integrations/slack.mjs +3 -0
- package/dist/integrations/vercel.d.mts +3 -0
- package/dist/integrations/vercel.mjs +3 -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 +46 -0
- package/dist/merge-resolution/eve-tools.mjs +117 -0
- package/dist/merge-resolution/eve-tools.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 +273 -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 +7 -1
- package/dist/postgres/index.mjs +7 -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 +53 -0
- package/dist/presets/software-development/dispatch.mjs +66 -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 +23 -0
- package/dist/presets/software-development/task-communication.mjs +120 -0
- package/dist/presets/software-development/task-communication.mjs.map +1 -0
- package/dist/presets/software-development.d.mts +9 -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 +53 -0
- package/dist/pull-requests/github-publisher.mjs +345 -0
- package/dist/pull-requests/github-publisher.mjs.map +1 -0
- package/dist/pull-requests.d.mts +5 -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 +9 -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 +22 -0
- package/dist/schema/agent-route.mjs +15 -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-DwCihQr4.mjs → schema/common.mjs} +7 -14
- package/dist/schema/common.mjs.map +1 -0
- package/dist/schema/communication.d.mts +52 -0
- package/dist/schema/communication.mjs +35 -0
- 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 +95 -0
- package/dist/schema/factory-config.mjs +60 -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 +26 -0
- package/dist/schema/repository.mjs +25 -0
- package/dist/schema/repository.mjs.map +1 -0
- package/dist/schema/session.d.mts +135 -0
- package/dist/schema/session.mjs +53 -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/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 +3134 -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 +272 -0
- package/dist/schema/task.mjs +199 -0
- package/dist/schema/task.mjs.map +1 -0
- package/dist/schema/transcript.d.mts +170 -0
- package/dist/schema/transcript.mjs +86 -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 +101 -0
- package/dist/schema/work.mjs +102 -0
- package/dist/schema/work.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 +19 -0
- package/dist/signals.mjs +19 -0
- package/dist/signals.mjs.map +1 -0
- package/dist/stall.d.mts +38 -0
- package/dist/stall.mjs +79 -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 +485 -0
- package/dist/store/engine.mjs +2175 -0
- package/dist/store/engine.mjs.map +1 -0
- package/dist/store/memory.d.mts +7 -0
- package/dist/store/memory.mjs +52 -0
- package/dist/store/memory.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 +65 -0
- package/dist/store/task-work.mjs +134 -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 +57 -0
- package/dist/sweep.mjs +143 -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 +264 -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 +21 -0
- package/dist/vercel-git.mjs +112 -0
- package/dist/vercel-git.mjs.map +1 -0
- package/dist/vercel.d.mts +33 -2
- package/dist/vercel.mjs +48 -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 +3 -0
- package/dist/work-triage.mjs +3 -0
- package/dist/workflows.d.mts +48 -0
- package/dist/workflows.mjs +86 -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-snapshot.d.mts +26 -0
- package/dist/workspace-snapshot.mjs +80 -0
- package/dist/workspace-snapshot.mjs.map +1 -0
- package/package.json +129 -40
- package/dist/approval-CwhTP1XB.mjs +0 -269
- package/dist/approval-CwhTP1XB.mjs.map +0 -1
- package/dist/change-CzEAoFVE.mjs +0 -620
- package/dist/change-CzEAoFVE.mjs.map +0 -1
- package/dist/dispatch-B77cuPxh.d.mts +0 -713
- package/dist/driver-BJutB0PE.d.mts +0 -34
- package/dist/driver-Bbd25rNM.mjs +0 -23
- package/dist/driver-Bbd25rNM.mjs.map +0 -1
- package/dist/factory-config-BJcjsPKl.mjs +0 -86
- package/dist/factory-config-BJcjsPKl.mjs.map +0 -1
- package/dist/factory-config-D2yU3G0c.d.mts +0 -58
- package/dist/github-DXsxboUL.d.mts +0 -271
- package/dist/id-DwCihQr4.mjs.map +0 -1
- package/dist/index-Cy4jfXh5.d.mts +0 -67
- package/dist/index.mjs.map +0 -1
- package/dist/signal-CHVEmzvI.d.mts +0 -59
|
@@ -0,0 +1,2175 @@
|
|
|
1
|
+
import { answerIdSchema, changeIdSchema, outboxIdSchema, repositoryIdsSchema, taskIdSchema } from "../schema/id.mjs";
|
|
2
|
+
import { replyToSchema, usdSchema } from "../schema/common.mjs";
|
|
3
|
+
import { repositorySchema } from "../schema/repository.mjs";
|
|
4
|
+
import { budgetSchema } from "../schema/budget.mjs";
|
|
5
|
+
import { parseAgentRouteBinding } from "../schema/agent-route.mjs";
|
|
6
|
+
import { jsonValueSchema, taskWorkSchema } from "../schema/work.mjs";
|
|
7
|
+
import { taskKindSchema, taskOriginSchema, taskQuestionSchema, taskSchema } from "../schema/task.mjs";
|
|
8
|
+
import { InvalidTransitionError, assertTransition, changeInitialState, changeTransitions, legalNextStates, signalOutcomeTransitions, signalTriageTransitions, taskInitialState, taskTransitions } from "../schema/transitions.mjs";
|
|
9
|
+
import { costSchema, receiptSchema } from "../schema/receipt.mjs";
|
|
10
|
+
import { imageAttachmentSchema, imageAttachmentsSchema, imageIdSchema, imageUploadSchema } from "../image-input.mjs";
|
|
11
|
+
import { transcriptSnapshotSchema } from "../schema/transcript.mjs";
|
|
12
|
+
import { routeTaskWork } from "../agent-routes.mjs";
|
|
13
|
+
import { RecordExistsError, RecordNotFoundError, TransientStoreError, VersionConflictError } from "./driver.mjs";
|
|
14
|
+
import { answerEventSchema } from "../schema/answer.mjs";
|
|
15
|
+
import { taskGraphDependenciesSchema } from "../schema/task-graph.mjs";
|
|
16
|
+
import { defineTaskMessageProtocols } from "../schema/task-message.mjs";
|
|
17
|
+
import { createTaskGraphStore } from "./task-graphs.mjs";
|
|
18
|
+
import { createTaskMessageStore } from "./task-messages.mjs";
|
|
19
|
+
import { createTaskWorkStore } from "./task-work.mjs";
|
|
20
|
+
import { createTaskEffectStore } from "./task-effects.mjs";
|
|
21
|
+
import { defaultTaskWorkflow, defineWorkflows } from "../workflows.mjs";
|
|
22
|
+
import { createWorkNotifier } from "./work-notifications.mjs";
|
|
23
|
+
import { communicationEventSchema } from "../schema/communication.mjs";
|
|
24
|
+
import { agentSessionIdSchema, agentSessionSchema } from "../schema/session.mjs";
|
|
25
|
+
import { externalSessionSchema, storedSessionSchema } from "../schema/stored-session.mjs";
|
|
26
|
+
import { changeSchema } from "../schema/change.mjs";
|
|
27
|
+
import { outboxMessageSchema, outboxQueueSchema } from "../schema/outbox.mjs";
|
|
28
|
+
import { storedTaskSchema } from "../schema/stored-task.mjs";
|
|
29
|
+
import { changeCatalogSchema, externalSessionCatalogSchema, sessionCatalogSchema, taskCatalogSchema, taskRouteSchema } from "../schema/catalog.mjs";
|
|
30
|
+
import { questionRouteSchema } from "../schema/question-route.mjs";
|
|
31
|
+
import { signalSchema } from "../schema/signal.mjs";
|
|
32
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
33
|
+
import { z } from "zod";
|
|
34
|
+
import { isDeepStrictEqual } from "node:util";
|
|
35
|
+
//#region src/store/engine.ts
|
|
36
|
+
const storedImageSchema = imageAttachmentSchema.extend({
|
|
37
|
+
schemaVersion: z.literal(1).describe("Stored image schema version."),
|
|
38
|
+
owner: z.string().min(1).describe("Authenticated image owner."),
|
|
39
|
+
base64: imageUploadSchema.shape.base64,
|
|
40
|
+
createdAt: z.iso.datetime().describe("Image creation time.")
|
|
41
|
+
}).refine((image) => {
|
|
42
|
+
const bytes = Buffer.from(image.base64, "base64");
|
|
43
|
+
if (bytes.length !== image.size || bytes.toString("base64") !== image.base64) return false;
|
|
44
|
+
if (image.mediaType === "image/png") return bytes.length >= 24 && bytes.subarray(0, 8).equals(Buffer.from([
|
|
45
|
+
137,
|
|
46
|
+
80,
|
|
47
|
+
78,
|
|
48
|
+
71,
|
|
49
|
+
13,
|
|
50
|
+
10,
|
|
51
|
+
26,
|
|
52
|
+
10
|
|
53
|
+
]));
|
|
54
|
+
if (image.mediaType === "image/jpeg") return bytes.length >= 4 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255;
|
|
55
|
+
return bytes.length >= 16 && bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP";
|
|
56
|
+
}, "Image bytes do not match the declared PNG, JPEG, or WebP format");
|
|
57
|
+
const storedTranscriptSchema = z.object({
|
|
58
|
+
schemaVersion: z.literal(1),
|
|
59
|
+
execution: z.object({
|
|
60
|
+
provider: z.string().min(1),
|
|
61
|
+
sessionId: z.string().min(1)
|
|
62
|
+
}),
|
|
63
|
+
snapshot: transcriptSnapshotSchema,
|
|
64
|
+
updatedAt: z.iso.datetime()
|
|
65
|
+
});
|
|
66
|
+
/** Error raised when uploaded image bytes, ownership, or attachment metadata is invalid. */
|
|
67
|
+
var ImageInputError = class extends Error {};
|
|
68
|
+
/** An approval decision that does not apply to the task's current approval or state. */
|
|
69
|
+
var ApprovalError = class extends Error {
|
|
70
|
+
taskId;
|
|
71
|
+
reason;
|
|
72
|
+
constructor(taskId, reason) {
|
|
73
|
+
super(`approval decision on "${taskId}" refused: ${reason}`);
|
|
74
|
+
this.name = "ApprovalError";
|
|
75
|
+
this.taskId = taskId;
|
|
76
|
+
this.reason = reason;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
/** A settlement that does not apply to the task's current state. */
|
|
80
|
+
var SettlementError = class extends Error {
|
|
81
|
+
taskId;
|
|
82
|
+
reason;
|
|
83
|
+
constructor(taskId, reason) {
|
|
84
|
+
super(`settlement on "${taskId}" refused: ${reason}`);
|
|
85
|
+
this.name = "SettlementError";
|
|
86
|
+
this.taskId = taskId;
|
|
87
|
+
this.reason = reason;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
/** Error raised when a Task is not authorized to create or update a requested Change. */
|
|
91
|
+
var ChangeAssociationError = class extends Error {
|
|
92
|
+
taskId;
|
|
93
|
+
repositoryId;
|
|
94
|
+
reason;
|
|
95
|
+
constructor(taskId, repositoryId, reason) {
|
|
96
|
+
super(`change for task "${taskId}" refused: ${reason}`);
|
|
97
|
+
this.name = "ChangeAssociationError";
|
|
98
|
+
this.taskId = taskId;
|
|
99
|
+
this.repositoryId = repositoryId;
|
|
100
|
+
this.reason = reason;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
/** Error raised when a stable outbox event ID is reused with different content. */
|
|
104
|
+
var OutboxEventConflictError = class extends Error {
|
|
105
|
+
constructor(eventId) {
|
|
106
|
+
super(`outbox event "${eventId}" was reused with different content`);
|
|
107
|
+
this.name = "OutboxEventConflictError";
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
/** Error raised when a QuestionTask already has a different immutable answer event. */
|
|
111
|
+
var AnswerEventConflictError = class extends Error {
|
|
112
|
+
constructor(questionTaskId) {
|
|
113
|
+
super(`question "${questionTaskId}" already has a different answer event`);
|
|
114
|
+
this.name = "AnswerEventConflictError";
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const createTaskInputSchema = z.strictObject({
|
|
118
|
+
repositoryIds: repositoryIdsSchema,
|
|
119
|
+
kind: taskKindSchema,
|
|
120
|
+
origin: taskOriginSchema,
|
|
121
|
+
replyTo: replyToSchema,
|
|
122
|
+
dedupeKey: z.string().min(1).refine((key) => key.trim().length > 0).optional().describe("Stable key for retryable task admission; omitted for one-shot creation."),
|
|
123
|
+
area: z.string().min(1).optional(),
|
|
124
|
+
changeId: changeIdSchema.optional(),
|
|
125
|
+
question: taskQuestionSchema.optional(),
|
|
126
|
+
approval: z.literal("required").optional().describe("Require approval before execution."),
|
|
127
|
+
work: taskWorkSchema,
|
|
128
|
+
parentTaskId: taskIdSchema.optional(),
|
|
129
|
+
parentTaskAttempt: z.number().int().positive().optional(),
|
|
130
|
+
dependencies: taskGraphDependenciesSchema.optional()
|
|
131
|
+
});
|
|
132
|
+
/** One completed model step's usage, as eve's step.completed event carries it. */
|
|
133
|
+
const stepUsageSchema = z.strictObject({
|
|
134
|
+
turnId: z.string().min(1).describe("Provider turn containing the completed step."),
|
|
135
|
+
stepIndex: z.number().int().nonnegative().describe("Nonnegative index of the completed step."),
|
|
136
|
+
costUsd: usdSchema.optional().describe("Measured cost for this step."),
|
|
137
|
+
inputTokens: z.number().int().nonnegative().optional().describe("Input tokens charged for this step."),
|
|
138
|
+
outputTokens: z.number().int().nonnegative().optional().describe("Output tokens charged for this step."),
|
|
139
|
+
cacheReadTokens: z.number().int().nonnegative().optional().describe("Cached input tokens read for this step."),
|
|
140
|
+
cacheWriteTokens: z.number().int().nonnegative().optional().describe("Input tokens written to cache for this step.")
|
|
141
|
+
});
|
|
142
|
+
/** Error raised when an idempotent session operation conflicts with its original intent. */
|
|
143
|
+
var SessionConflictError = class extends Error {};
|
|
144
|
+
function sameStoredValue(left, right) {
|
|
145
|
+
return isDeepStrictEqual(JSON.parse(JSON.stringify(left)), JSON.parse(JSON.stringify(right)));
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Whether an operator's answer matches one of the question's declared
|
|
149
|
+
* closing options (trimmed, case-insensitive). The asker judged that these
|
|
150
|
+
* exact replies mean no further attempt can help, so answering with one
|
|
151
|
+
* resolves the parent task instead of requeuing it for identical retry.
|
|
152
|
+
*/
|
|
153
|
+
function isClosingAnswer(closingOptions, answer) {
|
|
154
|
+
if (closingOptions === void 0 || closingOptions.length === 0) return false;
|
|
155
|
+
const normalized = answer.trim().toLowerCase();
|
|
156
|
+
return closingOptions.some((option) => option.trim().toLowerCase() === normalized);
|
|
157
|
+
}
|
|
158
|
+
const taskReceiptStates = {
|
|
159
|
+
queued: "queued",
|
|
160
|
+
running: "dispatched",
|
|
161
|
+
stalled: "stalled",
|
|
162
|
+
succeeded: "succeeded",
|
|
163
|
+
failed: "failed",
|
|
164
|
+
needs_human: "needs_human",
|
|
165
|
+
cancelled: "cancelled"
|
|
166
|
+
};
|
|
167
|
+
function needsLifecycleCommunication(task, state, phase) {
|
|
168
|
+
if (state === "needs_human") return task.question !== void 0;
|
|
169
|
+
if (task.parentTaskId !== void 0) return false;
|
|
170
|
+
if (state === "dispatched") return phase === "retry";
|
|
171
|
+
if (state === "failed" || state === "cancelled") return true;
|
|
172
|
+
if (state !== "succeeded") return false;
|
|
173
|
+
return task.question === void 0 && task.changeId === void 0;
|
|
174
|
+
}
|
|
175
|
+
const changeReceiptStates = {
|
|
176
|
+
verifying: "verifying",
|
|
177
|
+
ready: "ready",
|
|
178
|
+
merged: "merged",
|
|
179
|
+
deployed: "deployed",
|
|
180
|
+
rolled_out: "rolled_out",
|
|
181
|
+
rolled_back: "rolled_back"
|
|
182
|
+
};
|
|
183
|
+
function randomId(prefix) {
|
|
184
|
+
return `${prefix}_${randomUUID().replaceAll("-", "")}`;
|
|
185
|
+
}
|
|
186
|
+
function derivedId(prefix, key) {
|
|
187
|
+
return `${prefix}_${createHash("sha256").update(key).digest("hex").slice(0, 32)}`;
|
|
188
|
+
}
|
|
189
|
+
function bumpsAttempt(from, to) {
|
|
190
|
+
return to === "queued" || from === "verifying" && to === "running";
|
|
191
|
+
}
|
|
192
|
+
const emptyUsage = {
|
|
193
|
+
costUsd: 0,
|
|
194
|
+
inputTokens: 0,
|
|
195
|
+
outputTokens: 0,
|
|
196
|
+
cacheReadTokens: 0,
|
|
197
|
+
cacheWriteTokens: 0,
|
|
198
|
+
steps: {}
|
|
199
|
+
};
|
|
200
|
+
const unverifiedEvidence = {
|
|
201
|
+
reproVerified: false,
|
|
202
|
+
testsAdded: 0,
|
|
203
|
+
checksGreen: false,
|
|
204
|
+
attackPassed: false,
|
|
205
|
+
scopeMatches: false
|
|
206
|
+
};
|
|
207
|
+
function addUsd(a, b) {
|
|
208
|
+
return (Math.round(a * 1e6) + Math.round(b * 1e6)) / 1e6;
|
|
209
|
+
}
|
|
210
|
+
const TRANSIENT_RETRY_BASE_MS = 200;
|
|
211
|
+
const TRANSIENT_RETRY_MAX_TOTAL_MS = 1e4;
|
|
212
|
+
/**
|
|
213
|
+
* Backs off and returns the next attempt count, or rethrows `error` once
|
|
214
|
+
* the bound (attempts or total elapsed time) is reached.
|
|
215
|
+
*/
|
|
216
|
+
async function nextTransientRetry(error, attempt, startedAt) {
|
|
217
|
+
const nextDelayMs = TRANSIENT_RETRY_BASE_MS * 2 ** attempt;
|
|
218
|
+
if (attempt >= 4 || Date.now() - startedAt + nextDelayMs > TRANSIENT_RETRY_MAX_TOTAL_MS) throw error;
|
|
219
|
+
await new Promise((resolve) => setTimeout(resolve, nextDelayMs));
|
|
220
|
+
return attempt + 1;
|
|
221
|
+
}
|
|
222
|
+
/** Creates the validating Factory store engine over a deliberately policy-free driver. */
|
|
223
|
+
function createStores(options) {
|
|
224
|
+
const { driver } = options;
|
|
225
|
+
const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
226
|
+
const workflows = options.workflows ?? defineWorkflows([defaultTaskWorkflow]);
|
|
227
|
+
const notifications = createWorkNotifier(options.workNotifications);
|
|
228
|
+
function validateTaskWork(task) {
|
|
229
|
+
workflows.validateStoredInput(task.work.workflow, task.work.input);
|
|
230
|
+
if (task.workResult !== void 0) workflows.validateStoredOutput(task.work.workflow, task.workResult.output);
|
|
231
|
+
}
|
|
232
|
+
async function read(collection, id, schema) {
|
|
233
|
+
const record = await driver.get(collection, id);
|
|
234
|
+
if (record === null) return null;
|
|
235
|
+
const value = schema.parse(record.value);
|
|
236
|
+
if (collection === "tasks") {
|
|
237
|
+
const task = taskSchema.parse(value);
|
|
238
|
+
if (task.id !== id) throw new Error(`Task ${id} does not match its storage key`);
|
|
239
|
+
validateTaskWork(task);
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
value,
|
|
243
|
+
version: record.version
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
async function mustRead(collection, id, schema) {
|
|
247
|
+
const record = await read(collection, id, schema);
|
|
248
|
+
if (record === null) throw new RecordNotFoundError(collection, id);
|
|
249
|
+
return record;
|
|
250
|
+
}
|
|
251
|
+
async function listAll(collection, schema) {
|
|
252
|
+
return (await driver.list(collection)).map((entry) => schema.parse(entry.record.value));
|
|
253
|
+
}
|
|
254
|
+
async function forEachConcurrent(values, limit, operation) {
|
|
255
|
+
let next = 0;
|
|
256
|
+
await Promise.all(Array.from({ length: Math.min(limit, values.length) }, async () => {
|
|
257
|
+
while (next < values.length) await operation(values[next++]);
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
const catalogShards = "0123456789abcdef".split("");
|
|
261
|
+
const shardFor = (id) => createHash("sha256").update(id).digest("hex")[0];
|
|
262
|
+
function sessionSummary(session) {
|
|
263
|
+
return agentSessionSchema.parse({
|
|
264
|
+
...session,
|
|
265
|
+
continuations: session.continuations?.map(({ context: _context, ...continuation }) => continuation)
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
async function projectCatalogEntry(collection, prefix, schema, value, sourceVersion) {
|
|
269
|
+
const id = `${prefix}-${shardFor(value.id)}`;
|
|
270
|
+
for (;;) {
|
|
271
|
+
const existing = await driver.get(collection, id);
|
|
272
|
+
const parsed = existing === null ? null : schema.parse(existing.value);
|
|
273
|
+
const previous = parsed?.entries.find((entry) => entry.value.id === value.id);
|
|
274
|
+
const repairs = parsed?.repairs.filter((repair) => repair.id !== value.id || repair.sourceVersion > sourceVersion) ?? [];
|
|
275
|
+
if (previous !== void 0 && previous.sourceVersion >= sourceVersion && repairs.length === parsed?.repairs.length) return;
|
|
276
|
+
const entries = previous !== void 0 && previous.sourceVersion >= sourceVersion ? parsed?.entries ?? [] : [...parsed?.entries.filter((entry) => entry.value.id !== value.id) ?? [], {
|
|
277
|
+
sourceVersion,
|
|
278
|
+
value
|
|
279
|
+
}];
|
|
280
|
+
const next = schema.parse({
|
|
281
|
+
schemaVersion: 1,
|
|
282
|
+
id,
|
|
283
|
+
entries,
|
|
284
|
+
repairs,
|
|
285
|
+
updatedAt: now()
|
|
286
|
+
});
|
|
287
|
+
try {
|
|
288
|
+
if (existing === null) await driver.insert({
|
|
289
|
+
collection,
|
|
290
|
+
id,
|
|
291
|
+
value: next
|
|
292
|
+
});
|
|
293
|
+
else await driver.update({
|
|
294
|
+
collection,
|
|
295
|
+
id,
|
|
296
|
+
value: next,
|
|
297
|
+
expectedVersion: existing.version
|
|
298
|
+
});
|
|
299
|
+
return;
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (!(error instanceof RecordExistsError) && !(error instanceof VersionConflictError)) throw error;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const projectTaskCatalog = (value, sourceVersion) => projectCatalogEntry("taskCatalogs", "tasks", taskCatalogSchema, value, sourceVersion);
|
|
306
|
+
const projectChangeCatalog = (value, sourceVersion) => projectCatalogEntry("changeCatalogs", "changes", changeCatalogSchema, value, sourceVersion);
|
|
307
|
+
const projectSessionCatalog = (value, sourceVersion) => projectCatalogEntry("sessionCatalogs", "sessions", sessionCatalogSchema, sessionSummary(value), sourceVersion);
|
|
308
|
+
const projectExternalSessionCatalog = (value, sourceVersion) => projectCatalogEntry("externalSessionCatalogs", "external-sessions", externalSessionCatalogSchema, sessionSummary(value), sourceVersion);
|
|
309
|
+
async function markCatalogRepair(collection, prefix, schema, sourceId, sourceVersion) {
|
|
310
|
+
const id = `${prefix}-${shardFor(sourceId)}`;
|
|
311
|
+
for (;;) {
|
|
312
|
+
const existing = await driver.get(collection, id);
|
|
313
|
+
const parsed = existing === null ? null : schema.parse(existing.value);
|
|
314
|
+
const previous = parsed?.repairs.find((repair) => repair.id === sourceId);
|
|
315
|
+
if (previous !== void 0 && previous.sourceVersion >= sourceVersion) return;
|
|
316
|
+
const repairs = parsed?.repairs.filter((repair) => repair.id !== sourceId) ?? [];
|
|
317
|
+
const next = schema.parse({
|
|
318
|
+
schemaVersion: 1,
|
|
319
|
+
id,
|
|
320
|
+
entries: parsed?.entries ?? [],
|
|
321
|
+
repairs: [...repairs, {
|
|
322
|
+
id: sourceId,
|
|
323
|
+
sourceVersion
|
|
324
|
+
}],
|
|
325
|
+
updatedAt: now()
|
|
326
|
+
});
|
|
327
|
+
try {
|
|
328
|
+
if (existing === null) await driver.insert({
|
|
329
|
+
collection,
|
|
330
|
+
id,
|
|
331
|
+
value: next
|
|
332
|
+
});
|
|
333
|
+
else await driver.update({
|
|
334
|
+
collection,
|
|
335
|
+
id,
|
|
336
|
+
value: next,
|
|
337
|
+
expectedVersion: existing.version
|
|
338
|
+
});
|
|
339
|
+
return;
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (!(error instanceof RecordExistsError) && !(error instanceof VersionConflictError)) throw error;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const markTaskCatalogRepair = (id, sourceVersion) => markCatalogRepair("taskCatalogs", "tasks", taskCatalogSchema, id, sourceVersion);
|
|
346
|
+
const markChangeCatalogRepair = (id, sourceVersion) => markCatalogRepair("changeCatalogs", "changes", changeCatalogSchema, id, sourceVersion);
|
|
347
|
+
const markSessionCatalogRepair = (id, sourceVersion) => markCatalogRepair("sessionCatalogs", "sessions", sessionCatalogSchema, id, sourceVersion);
|
|
348
|
+
const markExternalSessionCatalogRepair = (id, sourceVersion) => markCatalogRepair("externalSessionCatalogs", "external-sessions", externalSessionCatalogSchema, id, sourceVersion);
|
|
349
|
+
async function projectTaskRoute(value, sourceVersion) {
|
|
350
|
+
if (value.replyTo.channel !== "api") return;
|
|
351
|
+
const id = derivedId("task-route", JSON.stringify([value.replyTo.channel, value.replyTo.address]));
|
|
352
|
+
for (;;) {
|
|
353
|
+
const existing = await read("taskRoutes", id, taskRouteSchema);
|
|
354
|
+
const previous = existing?.value.entries.find((entry) => entry.value.id === value.id);
|
|
355
|
+
const repairs = existing?.value.repairs.filter((repair) => repair.id !== value.id || repair.sourceVersion > sourceVersion) ?? [];
|
|
356
|
+
if (previous !== void 0 && previous.sourceVersion >= sourceVersion && repairs.length === existing?.value.repairs.length) return;
|
|
357
|
+
const entries = previous !== void 0 && previous.sourceVersion >= sourceVersion ? existing?.value.entries ?? [] : [...existing?.value.entries.filter((entry) => entry.value.id !== value.id) ?? [], {
|
|
358
|
+
sourceVersion,
|
|
359
|
+
value
|
|
360
|
+
}];
|
|
361
|
+
const next = taskRouteSchema.parse({
|
|
362
|
+
schemaVersion: 1,
|
|
363
|
+
id,
|
|
364
|
+
replyTo: value.replyTo,
|
|
365
|
+
entries,
|
|
366
|
+
repairs,
|
|
367
|
+
updatedAt: now()
|
|
368
|
+
});
|
|
369
|
+
try {
|
|
370
|
+
if (existing === null) await driver.insert({
|
|
371
|
+
collection: "taskRoutes",
|
|
372
|
+
id,
|
|
373
|
+
value: next
|
|
374
|
+
});
|
|
375
|
+
else await driver.update({
|
|
376
|
+
collection: "taskRoutes",
|
|
377
|
+
id,
|
|
378
|
+
value: next,
|
|
379
|
+
expectedVersion: existing.version
|
|
380
|
+
});
|
|
381
|
+
return;
|
|
382
|
+
} catch (error) {
|
|
383
|
+
if (!(error instanceof RecordExistsError) && !(error instanceof VersionConflictError)) throw error;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
async function markTaskRouteRepair(value, sourceVersion) {
|
|
388
|
+
if (value.replyTo.channel !== "api") return;
|
|
389
|
+
const id = derivedId("task-route", JSON.stringify([value.replyTo.channel, value.replyTo.address]));
|
|
390
|
+
for (;;) {
|
|
391
|
+
const existing = await read("taskRoutes", id, taskRouteSchema);
|
|
392
|
+
const previous = existing?.value.repairs.find((repair) => repair.id === value.id);
|
|
393
|
+
if (previous !== void 0 && previous.sourceVersion >= sourceVersion) return;
|
|
394
|
+
const repairs = existing?.value.repairs.filter((repair) => repair.id !== value.id) ?? [];
|
|
395
|
+
const next = taskRouteSchema.parse({
|
|
396
|
+
schemaVersion: 1,
|
|
397
|
+
id,
|
|
398
|
+
replyTo: value.replyTo,
|
|
399
|
+
entries: existing?.value.entries ?? [],
|
|
400
|
+
repairs: [...repairs, {
|
|
401
|
+
id: value.id,
|
|
402
|
+
sourceVersion
|
|
403
|
+
}],
|
|
404
|
+
updatedAt: now()
|
|
405
|
+
});
|
|
406
|
+
try {
|
|
407
|
+
if (existing === null) await driver.insert({
|
|
408
|
+
collection: "taskRoutes",
|
|
409
|
+
id,
|
|
410
|
+
value: next
|
|
411
|
+
});
|
|
412
|
+
else await driver.update({
|
|
413
|
+
collection: "taskRoutes",
|
|
414
|
+
id,
|
|
415
|
+
value: next,
|
|
416
|
+
expectedVersion: existing.version
|
|
417
|
+
});
|
|
418
|
+
return;
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if (!(error instanceof RecordExistsError) && !(error instanceof VersionConflictError)) throw error;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
async function prepareTaskProjection(value, sourceVersion) {
|
|
425
|
+
await Promise.all([markTaskCatalogRepair(value.id, sourceVersion), markTaskRouteRepair(value, sourceVersion)]);
|
|
426
|
+
}
|
|
427
|
+
async function projectTask(value, sourceVersion) {
|
|
428
|
+
await Promise.all([projectTaskCatalog(value, sourceVersion), projectTaskRoute(value, sourceVersion)]);
|
|
429
|
+
}
|
|
430
|
+
async function projectSafely(project) {
|
|
431
|
+
try {
|
|
432
|
+
await project();
|
|
433
|
+
} catch {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async function catalogValues(source, collection, prefix, sourceSchema, catalogSchema, project, fallbackValue = (value) => value) {
|
|
438
|
+
let catalogs = await Promise.all(catalogShards.map((shard) => driver.get(collection, `${prefix}-${shard}`)));
|
|
439
|
+
if (catalogs.some((catalog) => catalog === null)) {
|
|
440
|
+
const entries = await driver.list(source);
|
|
441
|
+
const values = entries.map((entry) => fallbackValue(sourceSchema.parse(entry.record.value)));
|
|
442
|
+
let projectionFailed = false;
|
|
443
|
+
await forEachConcurrent(entries, 16, async (entry) => {
|
|
444
|
+
try {
|
|
445
|
+
await project(sourceSchema.parse(entry.record.value), entry.record.version);
|
|
446
|
+
} catch {
|
|
447
|
+
projectionFailed = true;
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
if (!projectionFailed) for (const shard of catalogShards) {
|
|
451
|
+
const id = `${prefix}-${shard}`;
|
|
452
|
+
if (await driver.get(collection, id) !== null) continue;
|
|
453
|
+
const empty = catalogSchema.parse({
|
|
454
|
+
schemaVersion: 1,
|
|
455
|
+
id,
|
|
456
|
+
entries: [],
|
|
457
|
+
updatedAt: now()
|
|
458
|
+
});
|
|
459
|
+
try {
|
|
460
|
+
await driver.insert({
|
|
461
|
+
collection,
|
|
462
|
+
id,
|
|
463
|
+
value: empty
|
|
464
|
+
});
|
|
465
|
+
} catch (error) {
|
|
466
|
+
if (!(error instanceof RecordExistsError)) projectionFailed = true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (projectionFailed) return values;
|
|
470
|
+
catalogs = await Promise.all(catalogShards.map((shard) => driver.get(collection, `${prefix}-${shard}`)));
|
|
471
|
+
if (catalogs.some((catalog) => catalog === null)) return values;
|
|
472
|
+
}
|
|
473
|
+
const parsedCatalogs = catalogs.flatMap((catalog) => catalog === null ? [] : [catalogSchema.parse(catalog.value)]);
|
|
474
|
+
const values = /* @__PURE__ */ new Map();
|
|
475
|
+
for (const catalog of parsedCatalogs) for (const entry of catalog.entries) {
|
|
476
|
+
const value = sourceSchema.parse(entry.value);
|
|
477
|
+
values.set(value.id, value);
|
|
478
|
+
}
|
|
479
|
+
const repairs = /* @__PURE__ */ new Map();
|
|
480
|
+
for (const catalog of parsedCatalogs) for (const repair of catalog.repairs) repairs.set(repair.id, Math.max(repairs.get(repair.id) ?? 0, repair.sourceVersion));
|
|
481
|
+
await forEachConcurrent([...repairs.keys()], 16, async (id) => {
|
|
482
|
+
const record = await driver.get(source, id);
|
|
483
|
+
if (record === null) {
|
|
484
|
+
values.delete(id);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const canonical = sourceSchema.parse(record.value);
|
|
488
|
+
values.set(id, fallbackValue(canonical));
|
|
489
|
+
await projectSafely(() => project(canonical, record.version));
|
|
490
|
+
});
|
|
491
|
+
return [...values.values()];
|
|
492
|
+
}
|
|
493
|
+
async function updateTask(input, expectedVersion) {
|
|
494
|
+
const value = taskSchema.parse(input);
|
|
495
|
+
validateTaskWork(value);
|
|
496
|
+
const previous = options.workNotifications ? await mustRead("tasks", value.id, storedTaskSchema) : null;
|
|
497
|
+
await prepareTaskProjection(value, expectedVersion + 1);
|
|
498
|
+
await driver.update({
|
|
499
|
+
collection: "tasks",
|
|
500
|
+
id: value.id,
|
|
501
|
+
value,
|
|
502
|
+
expectedVersion
|
|
503
|
+
});
|
|
504
|
+
await projectSafely(() => projectTask(value, expectedVersion + 1));
|
|
505
|
+
await notifications.task(value, previous?.value ?? null, expectedVersion + 1);
|
|
506
|
+
}
|
|
507
|
+
async function updateChange(value, expectedVersion) {
|
|
508
|
+
await markChangeCatalogRepair(value.id, expectedVersion + 1);
|
|
509
|
+
await driver.update({
|
|
510
|
+
collection: "changes",
|
|
511
|
+
id: value.id,
|
|
512
|
+
value,
|
|
513
|
+
expectedVersion
|
|
514
|
+
});
|
|
515
|
+
await projectSafely(() => projectChangeCatalog(value, expectedVersion + 1));
|
|
516
|
+
}
|
|
517
|
+
async function updateSession(value, expectedVersion) {
|
|
518
|
+
await markSessionCatalogRepair(value.id, expectedVersion + 1);
|
|
519
|
+
await driver.update({
|
|
520
|
+
collection: "sessions",
|
|
521
|
+
id: value.id,
|
|
522
|
+
value,
|
|
523
|
+
expectedVersion
|
|
524
|
+
});
|
|
525
|
+
await projectSafely(() => projectSessionCatalog(value, expectedVersion + 1));
|
|
526
|
+
}
|
|
527
|
+
async function insertReceipt(receiptId, receipt) {
|
|
528
|
+
const reason = receipt.reason?.trim().slice(0, 2e3) || void 0;
|
|
529
|
+
const full = receiptSchema.parse({
|
|
530
|
+
...receipt,
|
|
531
|
+
reason,
|
|
532
|
+
schemaVersion: 1,
|
|
533
|
+
receiptId,
|
|
534
|
+
recordedAt: now()
|
|
535
|
+
});
|
|
536
|
+
try {
|
|
537
|
+
await driver.insert({
|
|
538
|
+
collection: "receipts",
|
|
539
|
+
id: receiptId,
|
|
540
|
+
value: full
|
|
541
|
+
});
|
|
542
|
+
return full;
|
|
543
|
+
} catch (error) {
|
|
544
|
+
if (error instanceof RecordExistsError) return (await mustRead("receipts", receiptId, receiptSchema)).value;
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async function readApprovalReceipt(task) {
|
|
549
|
+
const current = await read("receipts", derivedId("rcpt", `task:${task.id}:approval`), receiptSchema);
|
|
550
|
+
if (current) return current.value;
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
async function decideApproval(id, state, decision) {
|
|
554
|
+
const by = z.string().trim().min(1).parse(decision.by);
|
|
555
|
+
const reason = state === "rejected" ? z.string().trim().min(1).max(2e3).parse(decision.reason) : decision.reason?.trim().slice(0, 2e3) || void 0;
|
|
556
|
+
const receiptId = derivedId("rcpt", `task:${id}:approval`);
|
|
557
|
+
for (;;) {
|
|
558
|
+
const existing = await mustRead("tasks", id, storedTaskSchema);
|
|
559
|
+
const task = existing.value;
|
|
560
|
+
const saved = await readApprovalReceipt(task);
|
|
561
|
+
if (saved && (saved.state !== state || saved.decidedBy !== by || saved.reason !== reason)) throw new ApprovalError(id, "a different decision has already been recorded");
|
|
562
|
+
const approval = state === "approved" ? "granted" : "denied";
|
|
563
|
+
if (saved && task.approval === approval) {
|
|
564
|
+
await projectSafely(() => projectTask(task, existing.version));
|
|
565
|
+
return task;
|
|
566
|
+
}
|
|
567
|
+
if (task.approval !== "required") throw new ApprovalError(id, `approval is ${task.approval ?? "not required"}`);
|
|
568
|
+
if (task.state !== "queued") throw new ApprovalError(id, `task is ${task.state}`);
|
|
569
|
+
const receipt = saved ?? await insertReceipt(receiptId, {
|
|
570
|
+
repositoryIds: task.repositoryIds,
|
|
571
|
+
taskId: id,
|
|
572
|
+
kind: task.kind,
|
|
573
|
+
state,
|
|
574
|
+
phase: "approval",
|
|
575
|
+
reason,
|
|
576
|
+
attempt: task.attempt,
|
|
577
|
+
decidedBy: by
|
|
578
|
+
});
|
|
579
|
+
if (receipt.state !== state || receipt.decidedBy !== by || receipt.reason !== reason) throw new ApprovalError(id, "a different decision has already been recorded");
|
|
580
|
+
if (state === "rejected") assertTransition(taskTransitions, {
|
|
581
|
+
from: task.state,
|
|
582
|
+
to: "cancelled"
|
|
583
|
+
});
|
|
584
|
+
let next = taskSchema.parse({
|
|
585
|
+
...task,
|
|
586
|
+
approval,
|
|
587
|
+
state: state === "rejected" ? "cancelled" : task.state,
|
|
588
|
+
updatedAt: now()
|
|
589
|
+
});
|
|
590
|
+
if (state === "rejected") {
|
|
591
|
+
const cancelled = await appendReceipt(`task:${id}:cancelled:${task.attempt}`, {
|
|
592
|
+
repositoryIds: task.repositoryIds,
|
|
593
|
+
taskId: id,
|
|
594
|
+
kind: task.kind,
|
|
595
|
+
state: "cancelled",
|
|
596
|
+
phase: "approval",
|
|
597
|
+
reason: reason ?? `rejected by ${by}`,
|
|
598
|
+
attempt: task.attempt
|
|
599
|
+
});
|
|
600
|
+
if (needsLifecycleCommunication(next, "cancelled", "approval")) next = taskSchema.parse({
|
|
601
|
+
...next,
|
|
602
|
+
pendingCommunicationReceiptIds: [.../* @__PURE__ */ new Set([...next.pendingCommunicationReceiptIds ?? [], cancelled.receiptId])]
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
await updateTask(next, existing.version);
|
|
607
|
+
return next;
|
|
608
|
+
} catch (error) {
|
|
609
|
+
if (!(error instanceof VersionConflictError)) throw error;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
async function appendReceipt(key, receipt) {
|
|
614
|
+
return insertReceipt(derivedId("rcpt", key), receipt);
|
|
615
|
+
}
|
|
616
|
+
async function setQuestionRoute(replyTo, questionTaskId, present) {
|
|
617
|
+
const id = derivedId("qroute", JSON.stringify([replyTo.channel, replyTo.address]));
|
|
618
|
+
for (;;) {
|
|
619
|
+
const existing = await read("questionRoutes", id, questionRouteSchema);
|
|
620
|
+
const currentIds = existing?.value.questionTaskIds ?? [];
|
|
621
|
+
const questionTaskIds = present ? [.../* @__PURE__ */ new Set([...currentIds, questionTaskId])] : currentIds.filter((candidate) => candidate !== questionTaskId);
|
|
622
|
+
if (existing !== null && questionTaskIds.length === existing.value.questionTaskIds.length) return;
|
|
623
|
+
const next = questionRouteSchema.parse({
|
|
624
|
+
schemaVersion: 1,
|
|
625
|
+
id,
|
|
626
|
+
replyTo,
|
|
627
|
+
questionTaskIds,
|
|
628
|
+
updatedAt: now()
|
|
629
|
+
});
|
|
630
|
+
try {
|
|
631
|
+
if (existing === null) await driver.insert({
|
|
632
|
+
collection: "questionRoutes",
|
|
633
|
+
id,
|
|
634
|
+
value: next
|
|
635
|
+
});
|
|
636
|
+
else await driver.update({
|
|
637
|
+
collection: "questionRoutes",
|
|
638
|
+
id,
|
|
639
|
+
value: next,
|
|
640
|
+
expectedVersion: existing.version
|
|
641
|
+
});
|
|
642
|
+
return;
|
|
643
|
+
} catch (error) {
|
|
644
|
+
if (!(error instanceof RecordExistsError) && !(error instanceof VersionConflictError)) throw error;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
const stores = {
|
|
649
|
+
driverName: driver.name,
|
|
650
|
+
workflowRecords: {
|
|
651
|
+
get: driver.get.bind(driver),
|
|
652
|
+
insert: driver.insert.bind(driver),
|
|
653
|
+
update: driver.update.bind(driver)
|
|
654
|
+
},
|
|
655
|
+
taskEffects: createTaskEffectStore({
|
|
656
|
+
driver,
|
|
657
|
+
now,
|
|
658
|
+
getTask: (id) => stores.tasks.get(id)
|
|
659
|
+
}),
|
|
660
|
+
graphs: createTaskGraphStore({
|
|
661
|
+
driver,
|
|
662
|
+
now,
|
|
663
|
+
getTask: (id) => stores.tasks.get(id),
|
|
664
|
+
validateTask: validateTaskWork,
|
|
665
|
+
notify: notifications.publish
|
|
666
|
+
}),
|
|
667
|
+
messages: createTaskMessageStore({
|
|
668
|
+
driver,
|
|
669
|
+
now: () => new Date(now()),
|
|
670
|
+
getTask: (id) => stores.tasks.get(id),
|
|
671
|
+
protocols: options.messages?.protocols ?? defineTaskMessageProtocols([]),
|
|
672
|
+
authorize: options.messages?.authorize ?? (() => false),
|
|
673
|
+
notify: notifications.publish
|
|
674
|
+
}),
|
|
675
|
+
work: createTaskWorkStore({
|
|
676
|
+
driver,
|
|
677
|
+
now,
|
|
678
|
+
stores: () => stores,
|
|
679
|
+
workflows,
|
|
680
|
+
update: async (task, version) => {
|
|
681
|
+
await updateTask(task, version);
|
|
682
|
+
},
|
|
683
|
+
restore: async (task) => {
|
|
684
|
+
validateTaskWork(task);
|
|
685
|
+
await prepareTaskProjection(task, 1);
|
|
686
|
+
try {
|
|
687
|
+
await driver.insert({
|
|
688
|
+
collection: "tasks",
|
|
689
|
+
id: task.id,
|
|
690
|
+
value: task
|
|
691
|
+
});
|
|
692
|
+
} catch (error) {
|
|
693
|
+
if (!(error instanceof RecordExistsError)) throw error;
|
|
694
|
+
}
|
|
695
|
+
const existing = await mustRead("tasks", task.id, taskSchema);
|
|
696
|
+
await projectSafely(() => projectTask(existing.value, existing.version));
|
|
697
|
+
await notifications.task(existing.value, null, existing.version);
|
|
698
|
+
return existing.value;
|
|
699
|
+
}
|
|
700
|
+
}),
|
|
701
|
+
transcripts: {
|
|
702
|
+
async get(execution) {
|
|
703
|
+
const record = await read("transcripts", derivedId("transcript", JSON.stringify([execution.provider, execution.sessionId])), storedTranscriptSchema);
|
|
704
|
+
if (record === null) return null;
|
|
705
|
+
if (record.value.execution.provider !== execution.provider || record.value.execution.sessionId !== execution.sessionId) throw new Error("Transcript execution key does not match its stored value");
|
|
706
|
+
return record.value.snapshot;
|
|
707
|
+
},
|
|
708
|
+
async put(execution, value) {
|
|
709
|
+
const snapshot = transcriptSnapshotSchema.parse(value);
|
|
710
|
+
const id = derivedId("transcript", JSON.stringify([execution.provider, execution.sessionId]));
|
|
711
|
+
for (;;) {
|
|
712
|
+
const existing = await read("transcripts", id, storedTranscriptSchema);
|
|
713
|
+
if (existing !== null && existing.value.snapshot.cursor >= snapshot.cursor) return existing.value.snapshot;
|
|
714
|
+
const next = storedTranscriptSchema.parse({
|
|
715
|
+
schemaVersion: 1,
|
|
716
|
+
execution,
|
|
717
|
+
snapshot,
|
|
718
|
+
updatedAt: now()
|
|
719
|
+
});
|
|
720
|
+
try {
|
|
721
|
+
if (existing === null) await driver.insert({
|
|
722
|
+
collection: "transcripts",
|
|
723
|
+
id,
|
|
724
|
+
value: next
|
|
725
|
+
});
|
|
726
|
+
else await driver.update({
|
|
727
|
+
collection: "transcripts",
|
|
728
|
+
id,
|
|
729
|
+
value: next,
|
|
730
|
+
expectedVersion: existing.version
|
|
731
|
+
});
|
|
732
|
+
return snapshot;
|
|
733
|
+
} catch (error) {
|
|
734
|
+
if (!(error instanceof RecordExistsError) && !(error instanceof VersionConflictError)) throw error;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
images: {
|
|
740
|
+
async put(owner, input) {
|
|
741
|
+
const parsed = imageUploadSchema.parse(input);
|
|
742
|
+
const id = imageIdSchema.parse(`img_${createHash("sha256").update(JSON.stringify([
|
|
743
|
+
owner,
|
|
744
|
+
parsed.filename,
|
|
745
|
+
parsed.mediaType,
|
|
746
|
+
parsed.base64
|
|
747
|
+
])).digest("hex")}`);
|
|
748
|
+
const image = storedImageSchema.parse({
|
|
749
|
+
...parsed,
|
|
750
|
+
id,
|
|
751
|
+
owner,
|
|
752
|
+
schemaVersion: 1,
|
|
753
|
+
size: Buffer.from(parsed.base64, "base64").length,
|
|
754
|
+
createdAt: now()
|
|
755
|
+
});
|
|
756
|
+
try {
|
|
757
|
+
await driver.insert({
|
|
758
|
+
collection: "images",
|
|
759
|
+
id,
|
|
760
|
+
value: image
|
|
761
|
+
});
|
|
762
|
+
} catch (error) {
|
|
763
|
+
if (!(error instanceof RecordExistsError)) throw error;
|
|
764
|
+
await stores.images.get(owner, id);
|
|
765
|
+
}
|
|
766
|
+
return imageAttachmentSchema.parse({
|
|
767
|
+
id,
|
|
768
|
+
filename: image.filename,
|
|
769
|
+
mediaType: image.mediaType,
|
|
770
|
+
size: image.size
|
|
771
|
+
});
|
|
772
|
+
},
|
|
773
|
+
async get(owner, id) {
|
|
774
|
+
imageIdSchema.parse(id);
|
|
775
|
+
const record = await driver.get("images", id);
|
|
776
|
+
if (!record) return null;
|
|
777
|
+
const image = storedImageSchema.parse(record.value);
|
|
778
|
+
return image.owner === owner ? image : null;
|
|
779
|
+
},
|
|
780
|
+
async validate(owner, attachments) {
|
|
781
|
+
imageAttachmentsSchema.parse(attachments);
|
|
782
|
+
return Promise.all(attachments.map(async (attachment) => {
|
|
783
|
+
const image = await stores.images.get(owner, attachment.id);
|
|
784
|
+
if (!image) throw new ImageInputError("Image is unavailable; upload it again");
|
|
785
|
+
const ref = {
|
|
786
|
+
id: image.id,
|
|
787
|
+
filename: image.filename,
|
|
788
|
+
mediaType: image.mediaType,
|
|
789
|
+
size: image.size
|
|
790
|
+
};
|
|
791
|
+
if (!sameStoredValue(ref, attachment)) throw new ImageInputError("Image metadata does not match the uploaded image");
|
|
792
|
+
return ref;
|
|
793
|
+
}));
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
sessions: {
|
|
797
|
+
async create({ operationId, ...input }) {
|
|
798
|
+
if (operationId.trim().length === 0 || operationId.length > 200) throw new Error("operationId must contain 1–200 characters");
|
|
799
|
+
const id = derivedId("ses", JSON.stringify([input.owner, operationId]));
|
|
800
|
+
const candidate = storedSessionSchema.parse({
|
|
801
|
+
...input,
|
|
802
|
+
id,
|
|
803
|
+
schemaVersion: 1,
|
|
804
|
+
createdAt: now()
|
|
805
|
+
});
|
|
806
|
+
await stores.images.validate(input.owner, input.attachments ?? []);
|
|
807
|
+
await markSessionCatalogRepair(candidate.id, 1);
|
|
808
|
+
try {
|
|
809
|
+
await driver.insert({
|
|
810
|
+
collection: "sessions",
|
|
811
|
+
id,
|
|
812
|
+
value: candidate
|
|
813
|
+
});
|
|
814
|
+
await projectSafely(() => projectSessionCatalog(candidate, 1));
|
|
815
|
+
return candidate;
|
|
816
|
+
} catch (error) {
|
|
817
|
+
if (!(error instanceof RecordExistsError)) throw error;
|
|
818
|
+
const record = await mustRead("sessions", id, storedSessionSchema);
|
|
819
|
+
await projectSafely(() => projectSessionCatalog(record.value, record.version));
|
|
820
|
+
const existing = record.value;
|
|
821
|
+
const { createdAt: _at, execution: _execution, taskId: _task, notifications: _notifications, continuations: _continuations, ...intent } = existing;
|
|
822
|
+
const { createdAt: _newAt, notifications: _newNotifications, ...nextIntent } = candidate;
|
|
823
|
+
if (!sameStoredValue(intent, nextIntent)) throw new SessionConflictError("Session operationId was reused with different input");
|
|
824
|
+
return existing;
|
|
825
|
+
}
|
|
826
|
+
},
|
|
827
|
+
async attachConversation(input) {
|
|
828
|
+
const id = derivedId("ses", JSON.stringify([
|
|
829
|
+
"conversation",
|
|
830
|
+
input.owner,
|
|
831
|
+
input.execution.provider,
|
|
832
|
+
input.execution.sessionId
|
|
833
|
+
]));
|
|
834
|
+
const candidate = externalSessionSchema.parse({
|
|
835
|
+
...input,
|
|
836
|
+
id,
|
|
837
|
+
schemaVersion: 1,
|
|
838
|
+
createdAt: input.createdAt ?? now()
|
|
839
|
+
});
|
|
840
|
+
await markExternalSessionCatalogRepair(id, 1);
|
|
841
|
+
try {
|
|
842
|
+
await driver.insert({
|
|
843
|
+
collection: "externalSessions",
|
|
844
|
+
id,
|
|
845
|
+
value: candidate
|
|
846
|
+
});
|
|
847
|
+
await projectSafely(() => projectExternalSessionCatalog(candidate, 1));
|
|
848
|
+
return candidate;
|
|
849
|
+
} catch (error) {
|
|
850
|
+
if (!(error instanceof RecordExistsError)) throw error;
|
|
851
|
+
const record = await mustRead("externalSessions", id, externalSessionSchema);
|
|
852
|
+
const existing = record.value;
|
|
853
|
+
if (existing.owner !== candidate.owner || existing.agent !== candidate.agent || !sameStoredValue(existing.external, candidate.external) || !sameStoredValue(existing.execution, candidate.execution)) throw new SessionConflictError("External conversation binding cannot be replaced");
|
|
854
|
+
await projectSafely(() => projectExternalSessionCatalog(existing, record.version));
|
|
855
|
+
return existing;
|
|
856
|
+
}
|
|
857
|
+
},
|
|
858
|
+
async attachTask(input) {
|
|
859
|
+
const task = (await mustRead("tasks", input.taskId, storedTaskSchema)).value;
|
|
860
|
+
if (task.repositoryIds.length !== 1) throw new SessionConflictError("Only single-repository tasks can be opened");
|
|
861
|
+
if (task.replyTo.channel === "api") {
|
|
862
|
+
const original = await stores.sessions.get(agentSessionIdSchema.parse(task.replyTo.address));
|
|
863
|
+
if (original?.owner !== input.owner) throw new SessionConflictError("Task belongs to another operator interaction");
|
|
864
|
+
const record = await mustRead("sessions", original.id, storedSessionSchema);
|
|
865
|
+
await projectSafely(() => projectSessionCatalog(record.value, record.version));
|
|
866
|
+
return original;
|
|
867
|
+
}
|
|
868
|
+
if (task.work.route === void 0) throw new SessionConflictError("Only tasks with an exact agent route can be opened");
|
|
869
|
+
const id = derivedId("ses", JSON.stringify([
|
|
870
|
+
"task",
|
|
871
|
+
input.owner,
|
|
872
|
+
task.id
|
|
873
|
+
]));
|
|
874
|
+
const candidate = storedSessionSchema.parse({
|
|
875
|
+
schemaVersion: 1,
|
|
876
|
+
id,
|
|
877
|
+
owner: input.owner,
|
|
878
|
+
agent: input.agent,
|
|
879
|
+
route: task.work.route,
|
|
880
|
+
repositoryId: task.repositoryIds[0],
|
|
881
|
+
message: input.message,
|
|
882
|
+
taskId: task.id,
|
|
883
|
+
attached: true,
|
|
884
|
+
createdAt: task.createdAt
|
|
885
|
+
});
|
|
886
|
+
await markSessionCatalogRepair(candidate.id, 1);
|
|
887
|
+
try {
|
|
888
|
+
await driver.insert({
|
|
889
|
+
collection: "sessions",
|
|
890
|
+
id,
|
|
891
|
+
value: candidate
|
|
892
|
+
});
|
|
893
|
+
await projectSafely(() => projectSessionCatalog(candidate, 1));
|
|
894
|
+
return candidate;
|
|
895
|
+
} catch (error) {
|
|
896
|
+
if (!(error instanceof RecordExistsError)) throw error;
|
|
897
|
+
const existing = await mustRead("sessions", id, storedSessionSchema);
|
|
898
|
+
await projectSafely(() => projectSessionCatalog(existing.value, existing.version));
|
|
899
|
+
return existing.value;
|
|
900
|
+
}
|
|
901
|
+
},
|
|
902
|
+
async cacheContinuationContext(id, input) {
|
|
903
|
+
const { taskId, context, attachments } = input;
|
|
904
|
+
for (;;) {
|
|
905
|
+
const record = await mustRead("sessions", id, storedSessionSchema);
|
|
906
|
+
const continuation = record.value.continuations?.find((item) => item.taskId === taskId);
|
|
907
|
+
if (!continuation) throw new SessionConflictError("Task is not a conversation continuation");
|
|
908
|
+
if (continuation.context !== void 0) return continuation.context;
|
|
909
|
+
await stores.images.validate(record.value.owner, attachments ?? []);
|
|
910
|
+
const next = storedSessionSchema.parse({
|
|
911
|
+
...record.value,
|
|
912
|
+
continuations: record.value.continuations?.map((item) => item.taskId === taskId ? {
|
|
913
|
+
...item,
|
|
914
|
+
context,
|
|
915
|
+
...attachments?.length ? { contextAttachments: attachments } : {}
|
|
916
|
+
} : item)
|
|
917
|
+
});
|
|
918
|
+
try {
|
|
919
|
+
await updateSession(next, record.version);
|
|
920
|
+
return context;
|
|
921
|
+
} catch (error) {
|
|
922
|
+
if (!(error instanceof VersionConflictError)) throw error;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
},
|
|
926
|
+
async continue(id, input) {
|
|
927
|
+
if (input.operationId.trim().length === 0 || input.operationId.length > 200) throw new SessionConflictError("operationId must contain 1–200 characters");
|
|
928
|
+
const dedupeKey = `session:${id}:follow-up:${input.operationId}`;
|
|
929
|
+
for (;;) {
|
|
930
|
+
const record = await mustRead("sessions", id, storedSessionSchema);
|
|
931
|
+
const session = record.value;
|
|
932
|
+
await stores.images.validate(session.owner, input.attachments ?? []);
|
|
933
|
+
if (session.repositoryId === void 0 || session.taskId === void 0) throw new SessionConflictError("Follow-up work requires a repository-bound task conversation");
|
|
934
|
+
const parent = (await mustRead("tasks", input.parentTaskId, storedTaskSchema)).value;
|
|
935
|
+
if (parent.id !== session.taskId && !(parent.replyTo.channel === "api" && parent.replyTo.address === id)) throw new SessionConflictError("Follow-up parent is outside this conversation");
|
|
936
|
+
if (parent.repositoryIds.length !== 1 || parent.repositoryIds[0] !== session.repositoryId) throw new SessionConflictError("Follow-up repository differs from its conversation");
|
|
937
|
+
const previous = session.continuations?.find((item) => item.operationId === input.operationId);
|
|
938
|
+
if (previous !== void 0 && !sameStoredValue({
|
|
939
|
+
operationId: input.operationId,
|
|
940
|
+
parentTaskId: input.parentTaskId,
|
|
941
|
+
agent: input.agent,
|
|
942
|
+
message: input.message,
|
|
943
|
+
attachments: input.attachments ?? []
|
|
944
|
+
}, {
|
|
945
|
+
operationId: previous.operationId,
|
|
946
|
+
parentTaskId: previous.parentTaskId,
|
|
947
|
+
agent: previous.agent,
|
|
948
|
+
message: previous.message,
|
|
949
|
+
attachments: previous.attachments ?? []
|
|
950
|
+
})) throw new SessionConflictError("Follow-up operationId was reused with different input");
|
|
951
|
+
const continuation = previous ?? {
|
|
952
|
+
...input,
|
|
953
|
+
taskId: derivedId("task", JSON.stringify([
|
|
954
|
+
"task",
|
|
955
|
+
parent.id,
|
|
956
|
+
dedupeKey
|
|
957
|
+
])),
|
|
958
|
+
createdAt: now()
|
|
959
|
+
};
|
|
960
|
+
let next = session;
|
|
961
|
+
if (previous === void 0) {
|
|
962
|
+
if (![
|
|
963
|
+
"succeeded",
|
|
964
|
+
"failed",
|
|
965
|
+
"cancelled",
|
|
966
|
+
"verifying"
|
|
967
|
+
].includes(parent.state)) throw new SessionConflictError("Wait for this task or answer its pending question before starting a follow-up");
|
|
968
|
+
const latest = session.continuations?.at(-1);
|
|
969
|
+
if (latest !== void 0 && latest.taskId !== parent.id) {
|
|
970
|
+
let ancestor = parent;
|
|
971
|
+
const visited = /* @__PURE__ */ new Set();
|
|
972
|
+
while (ancestor && ancestor.id !== latest.taskId && !visited.has(ancestor.id)) {
|
|
973
|
+
visited.add(ancestor.id);
|
|
974
|
+
ancestor = ancestor.parentTaskId ? await stores.tasks.get(ancestor.parentTaskId) : null;
|
|
975
|
+
}
|
|
976
|
+
if (ancestor?.id !== latest.taskId) throw new SessionConflictError("Conversation changed; refresh before sending another follow-up");
|
|
977
|
+
const latestTask = await stores.tasks.get(latest.taskId);
|
|
978
|
+
if (latestTask === null || ![
|
|
979
|
+
"succeeded",
|
|
980
|
+
"failed",
|
|
981
|
+
"cancelled"
|
|
982
|
+
].includes(latestTask.state)) throw new SessionConflictError("A follow-up is already being started; refresh the conversation");
|
|
983
|
+
}
|
|
984
|
+
next = storedSessionSchema.parse({
|
|
985
|
+
...session,
|
|
986
|
+
continuations: [...session.continuations ?? [], continuation]
|
|
987
|
+
});
|
|
988
|
+
try {
|
|
989
|
+
await updateSession(next, record.version);
|
|
990
|
+
} catch (error) {
|
|
991
|
+
if (error instanceof VersionConflictError) continue;
|
|
992
|
+
throw error;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
const replyTo = {
|
|
996
|
+
channel: "api",
|
|
997
|
+
address: id
|
|
998
|
+
};
|
|
999
|
+
const signal = await stores.signals.create({
|
|
1000
|
+
repositoryId: session.repositoryId,
|
|
1001
|
+
source: "api",
|
|
1002
|
+
deliveryId: dedupeKey,
|
|
1003
|
+
replyTo,
|
|
1004
|
+
payload: {
|
|
1005
|
+
message: continuation.message,
|
|
1006
|
+
parentTaskId: parent.id
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
if (signal.outcome === "pending") try {
|
|
1010
|
+
await stores.signals.decide(signal.id, {
|
|
1011
|
+
outcome: "accepted",
|
|
1012
|
+
reason: `Follow-up requested by ${session.owner}`
|
|
1013
|
+
});
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
if (!(error instanceof VersionConflictError || error instanceof InvalidTransitionError)) throw error;
|
|
1016
|
+
}
|
|
1017
|
+
await stores.tasks.create({
|
|
1018
|
+
repositoryIds: [session.repositoryId],
|
|
1019
|
+
kind: "investigation",
|
|
1020
|
+
area: "conversation",
|
|
1021
|
+
origin: {
|
|
1022
|
+
operator: session.owner,
|
|
1023
|
+
signalId: signal.id
|
|
1024
|
+
},
|
|
1025
|
+
parentTaskId: parent.id,
|
|
1026
|
+
work: routeTaskWork({
|
|
1027
|
+
route: parseAgentRouteBinding({
|
|
1028
|
+
id: "conversation",
|
|
1029
|
+
version: 1
|
|
1030
|
+
}),
|
|
1031
|
+
title: "Conversation follow-up",
|
|
1032
|
+
input: { message: continuation.message }
|
|
1033
|
+
}),
|
|
1034
|
+
replyTo,
|
|
1035
|
+
dedupeKey,
|
|
1036
|
+
changeId: parent.changeId
|
|
1037
|
+
});
|
|
1038
|
+
return next;
|
|
1039
|
+
}
|
|
1040
|
+
},
|
|
1041
|
+
async get(id) {
|
|
1042
|
+
const records = (await Promise.all([read("sessions", id, storedSessionSchema), read("externalSessions", id, externalSessionSchema)])).filter((record) => record !== null);
|
|
1043
|
+
if (records.length > 1) throw new SessionConflictError("Session ID exists in multiple collections");
|
|
1044
|
+
return records[0]?.value ?? null;
|
|
1045
|
+
},
|
|
1046
|
+
async list() {
|
|
1047
|
+
return (await Promise.all([listAll("sessions", storedSessionSchema), listAll("externalSessions", externalSessionSchema)])).flat();
|
|
1048
|
+
},
|
|
1049
|
+
async summaries() {
|
|
1050
|
+
return (await Promise.all([catalogValues("sessions", "sessionCatalogs", "sessions", storedSessionSchema, sessionCatalogSchema, projectSessionCatalog, sessionSummary), catalogValues("externalSessions", "externalSessionCatalogs", "external-sessions", externalSessionSchema, externalSessionCatalogSchema, projectExternalSessionCatalog, sessionSummary)])).flat();
|
|
1051
|
+
},
|
|
1052
|
+
async bind(id, reference) {
|
|
1053
|
+
const external = await read("externalSessions", id, externalSessionSchema);
|
|
1054
|
+
if (external) {
|
|
1055
|
+
if (!("execution" in reference) || !sameStoredValue(external.value.execution, reference.execution)) throw new SessionConflictError("Session execution binding cannot be replaced");
|
|
1056
|
+
await projectSafely(() => projectExternalSessionCatalog(external.value, external.version));
|
|
1057
|
+
return external.value;
|
|
1058
|
+
}
|
|
1059
|
+
for (;;) {
|
|
1060
|
+
const existing = await mustRead("sessions", id, storedSessionSchema);
|
|
1061
|
+
if (reference.taskId !== void 0) {
|
|
1062
|
+
const task = (await mustRead("tasks", reference.taskId, storedTaskSchema)).value;
|
|
1063
|
+
if (task.origin.operator !== existing.value.owner || task.replyTo.channel !== "api" || task.replyTo.address !== id || task.repositoryIds[0] !== existing.value.repositoryId) throw new SessionConflictError("Task does not belong to this session");
|
|
1064
|
+
}
|
|
1065
|
+
const previous = existing.value.taskId === void 0 ? existing.value.execution === void 0 ? void 0 : { execution: existing.value.execution } : { taskId: existing.value.taskId };
|
|
1066
|
+
if (previous !== void 0) {
|
|
1067
|
+
if (!sameStoredValue(previous, reference)) throw new SessionConflictError("Session execution binding cannot be replaced");
|
|
1068
|
+
await projectSafely(() => projectSessionCatalog(existing.value, existing.version));
|
|
1069
|
+
return existing.value;
|
|
1070
|
+
}
|
|
1071
|
+
const next = storedSessionSchema.parse({
|
|
1072
|
+
...existing.value,
|
|
1073
|
+
...reference
|
|
1074
|
+
});
|
|
1075
|
+
try {
|
|
1076
|
+
await updateSession(next, existing.version);
|
|
1077
|
+
return next;
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
if (!(error instanceof VersionConflictError)) throw error;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
},
|
|
1083
|
+
async notify(input) {
|
|
1084
|
+
const event = communicationEventSchema.parse(input);
|
|
1085
|
+
if (event.replyTo.channel !== "api") throw new SessionConflictError("Expected an API notification");
|
|
1086
|
+
for (;;) {
|
|
1087
|
+
const record = await mustRead("sessions", event.replyTo.address, storedSessionSchema);
|
|
1088
|
+
const existing = record.value.notifications.find((item) => item.eventId === event.eventId);
|
|
1089
|
+
if (existing !== void 0) {
|
|
1090
|
+
if (!sameStoredValue(existing, event)) throw new SessionConflictError("Notification eventId was reused");
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
const next = storedSessionSchema.parse({
|
|
1094
|
+
...record.value,
|
|
1095
|
+
notifications: [...record.value.notifications, event]
|
|
1096
|
+
});
|
|
1097
|
+
try {
|
|
1098
|
+
await updateSession(next, record.version);
|
|
1099
|
+
return;
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
if (!(error instanceof VersionConflictError)) throw error;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
},
|
|
1106
|
+
repositories: {
|
|
1107
|
+
async put(repository) {
|
|
1108
|
+
const parsed = repositorySchema.parse(repository);
|
|
1109
|
+
const existing = await read("repositories", parsed.id, repositorySchema);
|
|
1110
|
+
if (existing === null) await driver.insert({
|
|
1111
|
+
collection: "repositories",
|
|
1112
|
+
id: parsed.id,
|
|
1113
|
+
value: parsed
|
|
1114
|
+
});
|
|
1115
|
+
else await driver.update({
|
|
1116
|
+
collection: "repositories",
|
|
1117
|
+
id: parsed.id,
|
|
1118
|
+
value: parsed,
|
|
1119
|
+
expectedVersion: existing.version
|
|
1120
|
+
});
|
|
1121
|
+
return parsed;
|
|
1122
|
+
},
|
|
1123
|
+
async get(id) {
|
|
1124
|
+
const record = await read("repositories", id, repositorySchema);
|
|
1125
|
+
return record === null ? null : record.value;
|
|
1126
|
+
},
|
|
1127
|
+
list() {
|
|
1128
|
+
return listAll("repositories", repositorySchema);
|
|
1129
|
+
}
|
|
1130
|
+
},
|
|
1131
|
+
signals: {
|
|
1132
|
+
async create(input) {
|
|
1133
|
+
const id = derivedId("sig", `${input.source}:${input.deliveryId}`);
|
|
1134
|
+
const signal = signalSchema.parse({
|
|
1135
|
+
schemaVersion: 1,
|
|
1136
|
+
id,
|
|
1137
|
+
repositoryId: input.repositoryId,
|
|
1138
|
+
source: input.source,
|
|
1139
|
+
deliveryId: input.deliveryId,
|
|
1140
|
+
replyTo: input.replyTo,
|
|
1141
|
+
payload: input.payload,
|
|
1142
|
+
receivedAt: now(),
|
|
1143
|
+
outcome: "pending"
|
|
1144
|
+
});
|
|
1145
|
+
try {
|
|
1146
|
+
await driver.insert({
|
|
1147
|
+
collection: "signals",
|
|
1148
|
+
id,
|
|
1149
|
+
value: signal
|
|
1150
|
+
});
|
|
1151
|
+
return signal;
|
|
1152
|
+
} catch (error) {
|
|
1153
|
+
if (error instanceof RecordExistsError) return (await mustRead("signals", id, signalSchema)).value;
|
|
1154
|
+
throw error;
|
|
1155
|
+
}
|
|
1156
|
+
},
|
|
1157
|
+
async get(id) {
|
|
1158
|
+
const record = await read("signals", id, signalSchema);
|
|
1159
|
+
return record === null ? null : record.value;
|
|
1160
|
+
},
|
|
1161
|
+
async beginTriage(id, options) {
|
|
1162
|
+
const existing = await mustRead("signals", id, signalSchema);
|
|
1163
|
+
const current = existing.value;
|
|
1164
|
+
if (current.outcome !== "pending" || (current.triage?.attempt ?? 0) !== options.expectedAttempt) throw new VersionConflictError("signals", id, existing.version);
|
|
1165
|
+
if (current.triage) assertTransition(signalTriageTransitions, {
|
|
1166
|
+
from: current.triage.state,
|
|
1167
|
+
to: "running"
|
|
1168
|
+
});
|
|
1169
|
+
const timestamp = now();
|
|
1170
|
+
const next = signalSchema.parse({
|
|
1171
|
+
...current,
|
|
1172
|
+
triage: {
|
|
1173
|
+
attempt: options.expectedAttempt + 1,
|
|
1174
|
+
state: "running",
|
|
1175
|
+
by: options.by,
|
|
1176
|
+
startedAt: timestamp,
|
|
1177
|
+
updatedAt: timestamp
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
await driver.update({
|
|
1181
|
+
collection: "signals",
|
|
1182
|
+
id,
|
|
1183
|
+
value: next,
|
|
1184
|
+
expectedVersion: existing.version
|
|
1185
|
+
});
|
|
1186
|
+
return next;
|
|
1187
|
+
},
|
|
1188
|
+
async updateTriage(id, attempt, observation) {
|
|
1189
|
+
for (let retry = 0; retry < 5; retry++) {
|
|
1190
|
+
const existing = await mustRead("signals", id, signalSchema);
|
|
1191
|
+
const current = existing.value;
|
|
1192
|
+
const triage = current.triage;
|
|
1193
|
+
if (current.outcome !== "pending" || triage?.attempt !== attempt || triage.state === "blocked") return current;
|
|
1194
|
+
if (triage.sessionId && observation.sessionId && triage.sessionId !== observation.sessionId) return current;
|
|
1195
|
+
if (triage.state !== observation.state) assertTransition(signalTriageTransitions, {
|
|
1196
|
+
from: triage.state,
|
|
1197
|
+
to: observation.state
|
|
1198
|
+
});
|
|
1199
|
+
const next = signalSchema.parse({
|
|
1200
|
+
...current,
|
|
1201
|
+
triage: {
|
|
1202
|
+
...triage,
|
|
1203
|
+
...observation,
|
|
1204
|
+
sessionId: observation.sessionId ?? triage.sessionId,
|
|
1205
|
+
updatedAt: now()
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
try {
|
|
1209
|
+
await driver.update({
|
|
1210
|
+
collection: "signals",
|
|
1211
|
+
id,
|
|
1212
|
+
value: next,
|
|
1213
|
+
expectedVersion: existing.version
|
|
1214
|
+
});
|
|
1215
|
+
return next;
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
if (!(error instanceof VersionConflictError) || retry === 4) throw error;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
throw new Error("Triage update retries exhausted");
|
|
1221
|
+
},
|
|
1222
|
+
async decide(id, input) {
|
|
1223
|
+
const { outcome, detail, reason } = input;
|
|
1224
|
+
const existing = await mustRead("signals", id, signalSchema);
|
|
1225
|
+
assertTransition(signalOutcomeTransitions, {
|
|
1226
|
+
from: existing.value.outcome,
|
|
1227
|
+
to: outcome
|
|
1228
|
+
});
|
|
1229
|
+
const next = signalSchema.parse({
|
|
1230
|
+
...existing.value,
|
|
1231
|
+
outcome,
|
|
1232
|
+
detail,
|
|
1233
|
+
reason,
|
|
1234
|
+
decidedAt: now()
|
|
1235
|
+
});
|
|
1236
|
+
await driver.update({
|
|
1237
|
+
collection: "signals",
|
|
1238
|
+
id,
|
|
1239
|
+
value: next,
|
|
1240
|
+
expectedVersion: existing.version
|
|
1241
|
+
});
|
|
1242
|
+
return next;
|
|
1243
|
+
},
|
|
1244
|
+
list() {
|
|
1245
|
+
return listAll("signals", signalSchema);
|
|
1246
|
+
}
|
|
1247
|
+
},
|
|
1248
|
+
tasks: {
|
|
1249
|
+
async create(value) {
|
|
1250
|
+
const input = createTaskInputSchema.parse(value);
|
|
1251
|
+
const timestamp = now();
|
|
1252
|
+
const id = input.dedupeKey === void 0 ? randomId("task") : derivedId("task", JSON.stringify([
|
|
1253
|
+
"task",
|
|
1254
|
+
input.parentTaskId ?? null,
|
|
1255
|
+
input.dedupeKey
|
|
1256
|
+
]));
|
|
1257
|
+
const parsedWork = structuredClone(taskWorkSchema.parse(input.work));
|
|
1258
|
+
const work = {
|
|
1259
|
+
...parsedWork,
|
|
1260
|
+
input: workflows.validateInput(parsedWork.workflow, parsedWork.input)
|
|
1261
|
+
};
|
|
1262
|
+
const parent = input.parentTaskId === void 0 ? null : await stores.tasks.get(input.parentTaskId);
|
|
1263
|
+
if (input.parentTaskId !== void 0 && parent === null) throw new Error("Child Tasks require an existing parent");
|
|
1264
|
+
if (input.parentTaskAttempt !== void 0 && (parent === null || input.question === void 0 || input.parentTaskAttempt !== parent.attempt)) throw new Error("Child questions must bind the current parent execution attempt");
|
|
1265
|
+
const origin = taskOriginSchema.parse(input.origin);
|
|
1266
|
+
let task = taskSchema.parse({
|
|
1267
|
+
schemaVersion: 1,
|
|
1268
|
+
id,
|
|
1269
|
+
repositoryIds: input.repositoryIds,
|
|
1270
|
+
kind: input.kind,
|
|
1271
|
+
area: input.area,
|
|
1272
|
+
state: taskInitialState,
|
|
1273
|
+
approval: input.approval,
|
|
1274
|
+
origin: origin.signalId === void 0 && origin.operator === void 0 && parent ? parent.origin : origin,
|
|
1275
|
+
work,
|
|
1276
|
+
rootTaskId: parent?.rootTaskId ?? id,
|
|
1277
|
+
parentTaskId: input.parentTaskId,
|
|
1278
|
+
parentTaskAttempt: input.question === void 0 ? void 0 : parent?.attempt,
|
|
1279
|
+
replyTo: input.replyTo,
|
|
1280
|
+
attempt: 1,
|
|
1281
|
+
question: input.question,
|
|
1282
|
+
changeId: input.changeId,
|
|
1283
|
+
createdAt: timestamp,
|
|
1284
|
+
updatedAt: timestamp
|
|
1285
|
+
});
|
|
1286
|
+
validateTaskWork(task);
|
|
1287
|
+
await stores.graphs.admit({
|
|
1288
|
+
task,
|
|
1289
|
+
dependencies: input.dependencies
|
|
1290
|
+
});
|
|
1291
|
+
const admission = (await stores.graphs.get(task.rootTaskId))?.nodes.find((node) => node.taskId === id)?.admission;
|
|
1292
|
+
if (admission === void 0) throw new Error(`Task ${id} has no durable graph admission`);
|
|
1293
|
+
task = admission;
|
|
1294
|
+
await prepareTaskProjection(task, 1);
|
|
1295
|
+
try {
|
|
1296
|
+
await driver.insert({
|
|
1297
|
+
collection: "tasks",
|
|
1298
|
+
id: task.id,
|
|
1299
|
+
value: task
|
|
1300
|
+
});
|
|
1301
|
+
await projectSafely(() => projectTask(task, 1));
|
|
1302
|
+
await notifications.task(task, null, 1);
|
|
1303
|
+
return task;
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
if (input.dedupeKey !== void 0 && error instanceof RecordExistsError) {
|
|
1306
|
+
const existing = await mustRead("tasks", task.id, storedTaskSchema);
|
|
1307
|
+
await projectSafely(() => projectTask(existing.value, existing.version));
|
|
1308
|
+
await notifications.task(existing.value, null, existing.version);
|
|
1309
|
+
return existing.value;
|
|
1310
|
+
}
|
|
1311
|
+
throw error;
|
|
1312
|
+
}
|
|
1313
|
+
},
|
|
1314
|
+
approve: (id, decision) => decideApproval(id, "approved", decision),
|
|
1315
|
+
reject: (id, decision) => decideApproval(id, "rejected", decision),
|
|
1316
|
+
async approvalReceipt(id) {
|
|
1317
|
+
return readApprovalReceipt((await mustRead("tasks", id, storedTaskSchema)).value);
|
|
1318
|
+
},
|
|
1319
|
+
async recordExecution(id, input) {
|
|
1320
|
+
const { execution, expectAttempt } = input;
|
|
1321
|
+
const startedAt = Date.now();
|
|
1322
|
+
for (let retries = 0;;) try {
|
|
1323
|
+
const existing = await mustRead("tasks", id, storedTaskSchema);
|
|
1324
|
+
const task = existing.value;
|
|
1325
|
+
if (task.state !== "running") throw new InvalidTransitionError(task.state, "running", legalNextStates(taskTransitions, task.state));
|
|
1326
|
+
if (expectAttempt !== void 0 && task.attempt !== expectAttempt) throw new Error(`task ${id} is on attempt ${task.attempt}, not ${expectAttempt}`);
|
|
1327
|
+
if (expectAttempt !== void 0 && task.execution) {
|
|
1328
|
+
if (!sameStoredValue(task.execution, execution)) throw new Error(`Task ${id} already has a different execution for attempt ${expectAttempt}`);
|
|
1329
|
+
return task;
|
|
1330
|
+
}
|
|
1331
|
+
const timestamp = now();
|
|
1332
|
+
const next = taskSchema.parse({
|
|
1333
|
+
...task,
|
|
1334
|
+
execution,
|
|
1335
|
+
lastProgressAt: timestamp,
|
|
1336
|
+
updatedAt: timestamp
|
|
1337
|
+
});
|
|
1338
|
+
await updateTask(next, existing.version);
|
|
1339
|
+
return next;
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
if (!(error instanceof TransientStoreError)) throw error;
|
|
1342
|
+
retries = await nextTransientRetry(error, retries, startedAt);
|
|
1343
|
+
}
|
|
1344
|
+
},
|
|
1345
|
+
async recordUsage(id, input) {
|
|
1346
|
+
const step = stepUsageSchema.parse(input.step);
|
|
1347
|
+
const { fence } = input;
|
|
1348
|
+
const startedAt = Date.now();
|
|
1349
|
+
let transientAttempt = 0;
|
|
1350
|
+
for (;;) {
|
|
1351
|
+
let existing;
|
|
1352
|
+
try {
|
|
1353
|
+
existing = await mustRead("tasks", id, storedTaskSchema);
|
|
1354
|
+
} catch (error) {
|
|
1355
|
+
if (!(error instanceof TransientStoreError)) throw error;
|
|
1356
|
+
transientAttempt = await nextTransientRetry(error, transientAttempt, startedAt);
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
const task = existing.value;
|
|
1360
|
+
if (fence && (task.attempt !== fence.attempt || !sameStoredValue(task.execution, fence.execution))) throw new Error(`Usage for task ${id} belongs to a replaced execution`);
|
|
1361
|
+
const usage = task.usage ?? emptyUsage;
|
|
1362
|
+
const recordedIndex = Object.hasOwn(usage.steps, step.turnId) ? usage.steps[step.turnId] : void 0;
|
|
1363
|
+
if (task.settledUsd !== void 0 && (recordedIndex === void 0 || step.stepIndex > recordedIndex)) throw new SettlementError(id, "task is already settled; new usage cannot be silently added");
|
|
1364
|
+
if (recordedIndex !== void 0 && step.stepIndex <= recordedIndex) {
|
|
1365
|
+
await projectSafely(() => projectTask(task, existing.version));
|
|
1366
|
+
return task;
|
|
1367
|
+
}
|
|
1368
|
+
const timestamp = now();
|
|
1369
|
+
const next = taskSchema.parse({
|
|
1370
|
+
...task,
|
|
1371
|
+
lastProgressAt: timestamp,
|
|
1372
|
+
usage: {
|
|
1373
|
+
costUsd: addUsd(usage.costUsd, step.costUsd ?? 0),
|
|
1374
|
+
inputTokens: usage.inputTokens + (step.inputTokens ?? 0),
|
|
1375
|
+
outputTokens: usage.outputTokens + (step.outputTokens ?? 0),
|
|
1376
|
+
cacheReadTokens: usage.cacheReadTokens + (step.cacheReadTokens ?? 0),
|
|
1377
|
+
cacheWriteTokens: usage.cacheWriteTokens + (step.cacheWriteTokens ?? 0),
|
|
1378
|
+
steps: {
|
|
1379
|
+
...usage.steps,
|
|
1380
|
+
[step.turnId]: step.stepIndex
|
|
1381
|
+
}
|
|
1382
|
+
},
|
|
1383
|
+
updatedAt: timestamp
|
|
1384
|
+
});
|
|
1385
|
+
try {
|
|
1386
|
+
await updateTask(next, existing.version);
|
|
1387
|
+
return next;
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
if (error instanceof VersionConflictError) continue;
|
|
1390
|
+
if (error instanceof TransientStoreError) {
|
|
1391
|
+
transientAttempt = await nextTransientRetry(error, transientAttempt, startedAt);
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
throw error;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
},
|
|
1398
|
+
async settle(id, input) {
|
|
1399
|
+
const existing = await mustRead("tasks", id, storedTaskSchema);
|
|
1400
|
+
const task = existing.value;
|
|
1401
|
+
if (task.settledUsd !== void 0) {
|
|
1402
|
+
await projectSafely(() => projectTask(task, existing.version));
|
|
1403
|
+
return task;
|
|
1404
|
+
}
|
|
1405
|
+
if (task.state !== "succeeded" && task.state !== "failed" && task.state !== "cancelled") throw new SettlementError(id, `task is ${task.state}, not terminal`);
|
|
1406
|
+
const validated = costSchema.parse(input.cost);
|
|
1407
|
+
const normalized = { totalUsd: Math.round(validated.totalUsd * 1e6) / 1e6 };
|
|
1408
|
+
let recorded = normalized;
|
|
1409
|
+
if (task.repositoryIds !== void 0) recorded = (await appendReceipt(`task:${id}:settled:${task.attempt}`, {
|
|
1410
|
+
repositoryIds: task.repositoryIds,
|
|
1411
|
+
taskId: id,
|
|
1412
|
+
changeId: task.changeId,
|
|
1413
|
+
kind: task.kind,
|
|
1414
|
+
state: "settled",
|
|
1415
|
+
phase: "budget",
|
|
1416
|
+
reason: input.reason,
|
|
1417
|
+
attempt: task.attempt,
|
|
1418
|
+
cost: normalized
|
|
1419
|
+
})).cost ?? normalized;
|
|
1420
|
+
const next = taskSchema.parse({
|
|
1421
|
+
...task,
|
|
1422
|
+
settledUsd: recorded.totalUsd,
|
|
1423
|
+
updatedAt: now()
|
|
1424
|
+
});
|
|
1425
|
+
await updateTask(next, existing.version);
|
|
1426
|
+
return next;
|
|
1427
|
+
},
|
|
1428
|
+
async get(id) {
|
|
1429
|
+
const record = await read("tasks", id, storedTaskSchema);
|
|
1430
|
+
return record === null ? null : record.value;
|
|
1431
|
+
},
|
|
1432
|
+
async findOpenQuestions(replyTo) {
|
|
1433
|
+
const route = await read("questionRoutes", derivedId("qroute", JSON.stringify([replyTo.channel, replyTo.address])), questionRouteSchema);
|
|
1434
|
+
if (route === null) return [];
|
|
1435
|
+
const openQuestions = (await Promise.all(route.value.questionTaskIds.map((taskId) => stores.tasks.get(taskId)))).filter((task) => task !== null && task.question !== void 0 && task.state === "needs_human" && task.replyTo.channel === replyTo.channel && task.replyTo.address === replyTo.address);
|
|
1436
|
+
const openIds = new Set(openQuestions.map((task) => task.id));
|
|
1437
|
+
for (const taskId of route.value.questionTaskIds) if (!openIds.has(taskId)) await setQuestionRoute(replyTo, taskId, false);
|
|
1438
|
+
return openQuestions;
|
|
1439
|
+
},
|
|
1440
|
+
async answerQuestion(input) {
|
|
1441
|
+
const question = await mustRead("tasks", input.questionTaskId, storedTaskSchema);
|
|
1442
|
+
if (question.value.question === void 0) throw new Error(`task ${input.questionTaskId} is not a question`);
|
|
1443
|
+
if (question.value.state !== "needs_human" && question.value.state !== "succeeded") throw new Error(`question task ${input.questionTaskId} is ${question.value.state}, not waiting`);
|
|
1444
|
+
const questionDetails = question.value.question;
|
|
1445
|
+
const id = answerIdSchema.parse(derivedId("ans", input.questionTaskId));
|
|
1446
|
+
const candidate = answerEventSchema.parse({
|
|
1447
|
+
schemaVersion: 1,
|
|
1448
|
+
id,
|
|
1449
|
+
questionTaskId: input.questionTaskId,
|
|
1450
|
+
parentTaskId: question.value.parentTaskId,
|
|
1451
|
+
repositoryIds: question.value.repositoryIds,
|
|
1452
|
+
replyTo: question.value.replyTo,
|
|
1453
|
+
question: questionDetails.text,
|
|
1454
|
+
answer: input.answer,
|
|
1455
|
+
answeredBy: input.answeredBy,
|
|
1456
|
+
recordedAt: now()
|
|
1457
|
+
});
|
|
1458
|
+
const answerOutput = jsonValueSchema.parse({
|
|
1459
|
+
answer: candidate.answer,
|
|
1460
|
+
answeredBy: candidate.answeredBy,
|
|
1461
|
+
questionTaskId: candidate.questionTaskId
|
|
1462
|
+
});
|
|
1463
|
+
let answerEvent = candidate;
|
|
1464
|
+
try {
|
|
1465
|
+
await driver.insert({
|
|
1466
|
+
collection: "answers",
|
|
1467
|
+
id,
|
|
1468
|
+
value: candidate
|
|
1469
|
+
});
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
if (!(error instanceof RecordExistsError)) throw error;
|
|
1472
|
+
const existing = await mustRead("answers", id, answerEventSchema);
|
|
1473
|
+
const { recordedAt: _existingAt, ...existingIntent } = existing.value;
|
|
1474
|
+
const { recordedAt: _candidateAt, ...candidateIntent } = candidate;
|
|
1475
|
+
if (!isDeepStrictEqual(existingIntent, candidateIntent)) throw new AnswerEventConflictError(input.questionTaskId);
|
|
1476
|
+
answerEvent = existing.value;
|
|
1477
|
+
}
|
|
1478
|
+
let parentTask = null;
|
|
1479
|
+
if (answerEvent.parentTaskId !== void 0) {
|
|
1480
|
+
parentTask = await stores.tasks.get(answerEvent.parentTaskId);
|
|
1481
|
+
if (parentTask === null) throw new Error(`parent task ${answerEvent.parentTaskId} does not exist`);
|
|
1482
|
+
for (;;) {
|
|
1483
|
+
const parent = await mustRead("tasks", answerEvent.parentTaskId, storedTaskSchema);
|
|
1484
|
+
if (parent.value.answerEventIds?.includes(answerEvent.id) === true) {
|
|
1485
|
+
parentTask = parent.value;
|
|
1486
|
+
break;
|
|
1487
|
+
}
|
|
1488
|
+
const projected = taskSchema.parse({
|
|
1489
|
+
...parent.value,
|
|
1490
|
+
answerEventIds: [...parent.value.answerEventIds ?? [], answerEvent.id],
|
|
1491
|
+
updatedAt: now()
|
|
1492
|
+
});
|
|
1493
|
+
try {
|
|
1494
|
+
await updateTask(projected, parent.version);
|
|
1495
|
+
parentTask = projected;
|
|
1496
|
+
break;
|
|
1497
|
+
} catch (error) {
|
|
1498
|
+
if (!(error instanceof VersionConflictError)) throw error;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
if (parentTask.state === "needs_human" && parentTask.questionTaskId === input.questionTaskId) {
|
|
1502
|
+
const closes = isClosingAnswer(questionDetails.closingOptions, answerEvent.answer);
|
|
1503
|
+
parentTask = await stores.tasks.transition(parentTask.id, closes ? "cancelled" : "queued", {
|
|
1504
|
+
expectFrom: "needs_human",
|
|
1505
|
+
expectAttempt: parentTask.attempt,
|
|
1506
|
+
expectQuestionTaskId: input.questionTaskId,
|
|
1507
|
+
reason: closes ? `question ${answerEvent.questionTaskId} answered by ${answerEvent.answeredBy}: ${answerEvent.answer}` : `question ${answerEvent.questionTaskId} answered by ${answerEvent.answeredBy}`
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
let questionTask = await mustRead("tasks", input.questionTaskId, storedTaskSchema).then((record) => record.value);
|
|
1512
|
+
if (questionTask.state === "needs_human") questionTask = await stores.tasks.transition(questionTask.id, "succeeded", {
|
|
1513
|
+
patch: { question: {
|
|
1514
|
+
...questionDetails,
|
|
1515
|
+
answer: answerEvent.answer,
|
|
1516
|
+
answeredBy: answerEvent.answeredBy
|
|
1517
|
+
} },
|
|
1518
|
+
output: answerOutput,
|
|
1519
|
+
reason: "question answered by authenticated operator"
|
|
1520
|
+
});
|
|
1521
|
+
else if (questionTask.state !== "succeeded" || questionTask.question?.answer !== answerEvent.answer || questionTask.question.answeredBy !== answerEvent.answeredBy) throw new Error(`question task ${questionTask.id} is ${questionTask.state}, not waiting`);
|
|
1522
|
+
return {
|
|
1523
|
+
answerEvent,
|
|
1524
|
+
questionTask,
|
|
1525
|
+
parentTask
|
|
1526
|
+
};
|
|
1527
|
+
},
|
|
1528
|
+
async transition(id, to, ...args) {
|
|
1529
|
+
const transitionOptions = args[0] ?? {};
|
|
1530
|
+
if (transitionOptions.patch !== void 0 && "settledUsd" in transitionOptions.patch) throw new SettlementError(id, "settledUsd is set only by settle, not a transition");
|
|
1531
|
+
if (transitionOptions.patch !== void 0 && "usage" in transitionOptions.patch) throw new SettlementError(id, "usage is set only by recordUsage, not a transition");
|
|
1532
|
+
let existing = await mustRead("tasks", id, storedTaskSchema);
|
|
1533
|
+
if (transitionOptions.expectFrom !== void 0 && existing.value.state !== transitionOptions.expectFrom) throw new InvalidTransitionError(existing.value.state, to, legalNextStates(taskTransitions, existing.value.state));
|
|
1534
|
+
if (transitionOptions.expectAttempt !== void 0 && existing.value.attempt !== transitionOptions.expectAttempt) throw new Error(`task ${id} is on attempt ${existing.value.attempt}, not ${transitionOptions.expectAttempt}`);
|
|
1535
|
+
if (transitionOptions.expectExecution !== void 0 && !sameStoredValue(existing.value.execution, transitionOptions.expectExecution)) throw new Error(`Task ${id} execution has been replaced`);
|
|
1536
|
+
if (transitionOptions.expectQuestionTaskId !== void 0 && existing.value.questionTaskId !== transitionOptions.expectQuestionTaskId) throw new Error(`task ${id} is no longer waiting for question ${transitionOptions.expectQuestionTaskId}`);
|
|
1537
|
+
assertTransition(taskTransitions, {
|
|
1538
|
+
from: existing.value.state,
|
|
1539
|
+
to
|
|
1540
|
+
});
|
|
1541
|
+
if (transitionOptions.patch !== void 0 && Object.keys(transitionOptions.patch).some((key) => ![
|
|
1542
|
+
"changeId",
|
|
1543
|
+
"reportRef",
|
|
1544
|
+
"reservedUsd",
|
|
1545
|
+
"reservedAt",
|
|
1546
|
+
"question"
|
|
1547
|
+
].includes(key))) throw new Error("Task transitions cannot replace workflow-owned fields");
|
|
1548
|
+
if (to !== "succeeded" && (transitionOptions.output !== void 0 || transitionOptions.reuseRecordedOutput === true)) throw new Error("Workflow output can only be recorded when succeeding");
|
|
1549
|
+
if (to === "succeeded" && transitionOptions.output === void 0 && transitionOptions.reuseRecordedOutput !== true) throw new Error("Succeeded transitions require output or reuseRecordedOutput after work completion");
|
|
1550
|
+
if (to === "succeeded" && transitionOptions.output !== void 0 && transitionOptions.reuseRecordedOutput === true) throw new Error("Choose output or reuseRecordedOutput, not both");
|
|
1551
|
+
if (to !== "needs_human" && (transitionOptions.needsHumanAction !== void 0 || transitionOptions.questionTaskId !== void 0)) throw new Error("Question context is only valid when entering needs_human");
|
|
1552
|
+
if (to === "running") {
|
|
1553
|
+
if (existing.value.approval !== void 0 && existing.value.approval !== "granted") throw new Error(`Task ${id} requires approval before execution`);
|
|
1554
|
+
}
|
|
1555
|
+
const timestamp = now();
|
|
1556
|
+
let workResult = existing.value.workResult;
|
|
1557
|
+
if (to === "succeeded") {
|
|
1558
|
+
const output = transitionOptions.output === void 0 ? existing.value.workResult?.output : workflows.validateOutput(existing.value.work.workflow, transitionOptions.output);
|
|
1559
|
+
if (output === void 0) throw new Error(`Task ${id} requires validated output to succeed`);
|
|
1560
|
+
if (existing.value.workResult && !isDeepStrictEqual(existing.value.workResult.output, output)) throw new Error(`Task ${id} already recorded a different workflow output`);
|
|
1561
|
+
workResult ??= {
|
|
1562
|
+
output,
|
|
1563
|
+
completedAt: timestamp
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
if (to === "stalled" && existing.value.waitingForMessages) throw new Error(`Task ${id} is waiting for messages, not stalled execution`);
|
|
1567
|
+
if (bumpsAttempt(existing.value.state, to) && existing.value.settledUsd !== void 0) throw new Error(`Task ${id} is settled; create new work rather than silently losing retry costs`);
|
|
1568
|
+
if (to === "needs_human" && existing.value.question === void 0) {
|
|
1569
|
+
if (transitionOptions.questionTaskId === void 0) throw new Error(`task ${id} cannot enter needs_human without a QuestionTask id`);
|
|
1570
|
+
const question = await mustRead("tasks", transitionOptions.questionTaskId, storedTaskSchema);
|
|
1571
|
+
if (question.value.question === void 0 || question.value.state !== "needs_human" || question.value.parentTaskId !== id || question.value.rootTaskId !== existing.value.rootTaskId || question.value.parentTaskAttempt !== existing.value.attempt || !sameStoredValue(question.value.replyTo, existing.value.replyTo)) throw new Error(`task ${id} cannot enter needs_human for question ${question.value.id}`);
|
|
1572
|
+
}
|
|
1573
|
+
const needsHumanAction = transitionOptions.needsHumanAction?.trim();
|
|
1574
|
+
if (to === "needs_human" && (needsHumanAction === void 0 || needsHumanAction === "")) throw new Error(`task ${id} cannot enter needs_human without an actionable needsHumanAction`);
|
|
1575
|
+
const bumps = bumpsAttempt(existing.value.state, to);
|
|
1576
|
+
const { stalledAt: _stalledAt, stalledReason: _stalledReason, questionTaskId: _questionTaskId, needsHumanAction: _needsHumanAction, needsHumanNotifiedAt: _needsHumanNotifiedAt, failureReceiptId: _failureReceiptId, ...cleared } = existing.value;
|
|
1577
|
+
let next = taskSchema.parse({
|
|
1578
|
+
...cleared,
|
|
1579
|
+
workResult,
|
|
1580
|
+
...bumps ? {
|
|
1581
|
+
execution: void 0,
|
|
1582
|
+
workResult: void 0
|
|
1583
|
+
} : {},
|
|
1584
|
+
waitingForMessages: void 0,
|
|
1585
|
+
...transitionOptions.patch,
|
|
1586
|
+
...to === "needs_human" && existing.value.question === void 0 ? { questionTaskId: transitionOptions.questionTaskId } : {},
|
|
1587
|
+
...to === "needs_human" ? { needsHumanAction } : {},
|
|
1588
|
+
...to === "stalled" ? {
|
|
1589
|
+
stalledAt: timestamp,
|
|
1590
|
+
stalledReason: transitionOptions.reason
|
|
1591
|
+
} : {},
|
|
1592
|
+
...to === "running" ? { lastProgressAt: timestamp } : {},
|
|
1593
|
+
state: to,
|
|
1594
|
+
attempt: bumps ? existing.value.attempt + 1 : existing.value.attempt,
|
|
1595
|
+
updatedAt: timestamp
|
|
1596
|
+
});
|
|
1597
|
+
validateTaskWork(next);
|
|
1598
|
+
if (to === "running") await stores.graphs.seal({
|
|
1599
|
+
rootTaskId: existing.value.rootTaskId,
|
|
1600
|
+
taskId: id
|
|
1601
|
+
});
|
|
1602
|
+
if (existing.value.question !== void 0 && to === "needs_human") await setQuestionRoute(existing.value.replyTo, existing.value.id, true);
|
|
1603
|
+
await projectSafely(() => projectTask(existing.value, existing.version));
|
|
1604
|
+
if (to === "succeeded" && existing.value.workResult === void 0) {
|
|
1605
|
+
const prepared = taskSchema.parse({
|
|
1606
|
+
...existing.value,
|
|
1607
|
+
workResult: next.workResult,
|
|
1608
|
+
updatedAt: timestamp
|
|
1609
|
+
});
|
|
1610
|
+
await updateTask(prepared, existing.version);
|
|
1611
|
+
existing = {
|
|
1612
|
+
value: prepared,
|
|
1613
|
+
version: existing.version + 1
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
const receiptState = taskReceiptStates[to];
|
|
1617
|
+
if (receiptState !== void 0 && next.repositoryIds !== void 0) {
|
|
1618
|
+
const receipt = await appendReceipt(JSON.stringify([
|
|
1619
|
+
"task",
|
|
1620
|
+
id,
|
|
1621
|
+
receiptState,
|
|
1622
|
+
next.attempt,
|
|
1623
|
+
next.changeId ?? null,
|
|
1624
|
+
transitionOptions.phase ?? null,
|
|
1625
|
+
transitionOptions.reason ?? null
|
|
1626
|
+
]), {
|
|
1627
|
+
repositoryIds: next.repositoryIds,
|
|
1628
|
+
taskId: id,
|
|
1629
|
+
changeId: next.changeId,
|
|
1630
|
+
kind: next.kind,
|
|
1631
|
+
state: receiptState,
|
|
1632
|
+
phase: transitionOptions.phase,
|
|
1633
|
+
reason: transitionOptions.reason,
|
|
1634
|
+
attempt: next.attempt,
|
|
1635
|
+
execution: next.execution
|
|
1636
|
+
});
|
|
1637
|
+
if (to === "failed") next = taskSchema.parse({
|
|
1638
|
+
...next,
|
|
1639
|
+
failureReceiptId: receipt.receiptId
|
|
1640
|
+
});
|
|
1641
|
+
if (needsLifecycleCommunication(next, receiptState, transitionOptions.phase)) next = taskSchema.parse({
|
|
1642
|
+
...next,
|
|
1643
|
+
pendingCommunicationReceiptIds: [...next.pendingCommunicationReceiptIds ?? [], receipt.receiptId]
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
if (existing.value.question !== void 0 && existing.value.state === "needs_human" && to !== "needs_human" && existing.value.parentTaskId !== void 0) {
|
|
1647
|
+
const parent = await stores.tasks.get(existing.value.parentTaskId);
|
|
1648
|
+
if (parent?.state === "needs_human" && parent.questionTaskId === id && parent.attempt === existing.value.parentTaskAttempt) await stores.tasks.transition(parent.id, "queued", {
|
|
1649
|
+
expectFrom: "needs_human",
|
|
1650
|
+
expectAttempt: parent.attempt,
|
|
1651
|
+
expectQuestionTaskId: id,
|
|
1652
|
+
reason: `question ${id} is no longer waiting`
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
await updateTask(next, existing.version);
|
|
1656
|
+
if (existing.value.question !== void 0 && existing.value.state === "needs_human" && to !== "needs_human") await setQuestionRoute(existing.value.replyTo, existing.value.id, false);
|
|
1657
|
+
return next;
|
|
1658
|
+
},
|
|
1659
|
+
async acknowledgeCommunication(id, receiptId) {
|
|
1660
|
+
for (;;) {
|
|
1661
|
+
const existing = await mustRead("tasks", id, storedTaskSchema);
|
|
1662
|
+
const pending = existing.value.pendingCommunicationReceiptIds ?? [];
|
|
1663
|
+
if (!pending.includes(receiptId)) {
|
|
1664
|
+
await projectSafely(() => projectTask(existing.value, existing.version));
|
|
1665
|
+
return existing.value;
|
|
1666
|
+
}
|
|
1667
|
+
const remaining = pending.filter((candidate) => candidate !== receiptId);
|
|
1668
|
+
const { pendingCommunicationReceiptIds: _pending, ...task } = existing.value;
|
|
1669
|
+
const next = taskSchema.parse({
|
|
1670
|
+
...task,
|
|
1671
|
+
...remaining.length === 0 ? {} : { pendingCommunicationReceiptIds: remaining },
|
|
1672
|
+
updatedAt: now()
|
|
1673
|
+
});
|
|
1674
|
+
try {
|
|
1675
|
+
await updateTask(next, existing.version);
|
|
1676
|
+
return next;
|
|
1677
|
+
} catch (error) {
|
|
1678
|
+
if (!(error instanceof VersionConflictError)) throw error;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
},
|
|
1682
|
+
async recordNeedsHumanDelivery(id, deliveryOptions = {}) {
|
|
1683
|
+
const at = deliveryOptions.at ?? now();
|
|
1684
|
+
let question;
|
|
1685
|
+
for (;;) {
|
|
1686
|
+
const existing = await mustRead("tasks", id, storedTaskSchema);
|
|
1687
|
+
if (existing.value.question === void 0) throw new Error(`task ${id} is not a QuestionTask`);
|
|
1688
|
+
if (existing.value.state !== "needs_human" || deliveryOptions.attempt !== void 0 && existing.value.attempt !== deliveryOptions.attempt) return existing.value;
|
|
1689
|
+
if (existing.value.needsHumanNotifiedAt !== void 0) {
|
|
1690
|
+
question = existing.value;
|
|
1691
|
+
break;
|
|
1692
|
+
}
|
|
1693
|
+
const next = taskSchema.parse({
|
|
1694
|
+
...existing.value,
|
|
1695
|
+
needsHumanNotifiedAt: existing.value.needsHumanNotifiedAt ?? at,
|
|
1696
|
+
updatedAt: now()
|
|
1697
|
+
});
|
|
1698
|
+
try {
|
|
1699
|
+
await updateTask(next, existing.version);
|
|
1700
|
+
question = next;
|
|
1701
|
+
break;
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
if (!(error instanceof VersionConflictError)) throw error;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
const parentId = question.parentTaskId;
|
|
1707
|
+
if (parentId !== void 0) for (;;) {
|
|
1708
|
+
const parent = await mustRead("tasks", parentId, storedTaskSchema);
|
|
1709
|
+
if (parent.value.state !== "needs_human" || parent.value.questionTaskId !== id || parent.value.attempt !== question.parentTaskAttempt || parent.value.needsHumanNotifiedAt !== void 0) break;
|
|
1710
|
+
const current = await mustRead("tasks", id, storedTaskSchema);
|
|
1711
|
+
if (current.value.state !== "needs_human" || current.value.attempt !== question.attempt) break;
|
|
1712
|
+
const next = taskSchema.parse({
|
|
1713
|
+
...parent.value,
|
|
1714
|
+
needsHumanNotifiedAt: question.needsHumanNotifiedAt,
|
|
1715
|
+
updatedAt: now()
|
|
1716
|
+
});
|
|
1717
|
+
try {
|
|
1718
|
+
await updateTask(next, parent.version);
|
|
1719
|
+
break;
|
|
1720
|
+
} catch (error) {
|
|
1721
|
+
if (!(error instanceof VersionConflictError)) throw error;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
return question;
|
|
1725
|
+
},
|
|
1726
|
+
async forReplyTo(replyTo) {
|
|
1727
|
+
const id = derivedId("task-route", JSON.stringify([replyTo.channel, replyTo.address]));
|
|
1728
|
+
let route = await read("taskRoutes", id, taskRouteSchema);
|
|
1729
|
+
let discovered = [];
|
|
1730
|
+
if (route === null) {
|
|
1731
|
+
discovered = (await stores.tasks.list()).filter((task) => task.replyTo.channel === replyTo.channel && task.replyTo.address === replyTo.address);
|
|
1732
|
+
for (const task of discovered) {
|
|
1733
|
+
const record = await mustRead("tasks", task.id, storedTaskSchema);
|
|
1734
|
+
await projectSafely(() => projectTaskRoute(record.value, record.version));
|
|
1735
|
+
}
|
|
1736
|
+
if (discovered.length === 0) {
|
|
1737
|
+
const empty = taskRouteSchema.parse({
|
|
1738
|
+
schemaVersion: 1,
|
|
1739
|
+
id,
|
|
1740
|
+
replyTo,
|
|
1741
|
+
entries: [],
|
|
1742
|
+
updatedAt: now()
|
|
1743
|
+
});
|
|
1744
|
+
await projectSafely(async () => {
|
|
1745
|
+
try {
|
|
1746
|
+
await driver.insert({
|
|
1747
|
+
collection: "taskRoutes",
|
|
1748
|
+
id,
|
|
1749
|
+
value: empty
|
|
1750
|
+
});
|
|
1751
|
+
} catch (error) {
|
|
1752
|
+
if (!(error instanceof RecordExistsError)) throw error;
|
|
1753
|
+
}
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
route = await read("taskRoutes", id, taskRouteSchema);
|
|
1757
|
+
}
|
|
1758
|
+
if (route === null) return discovered;
|
|
1759
|
+
const values = new Map(route.value.entries.map((entry) => [entry.value.id, entry.value]));
|
|
1760
|
+
await forEachConcurrent(route.value.repairs, 16, async (repair) => {
|
|
1761
|
+
const repairTaskId = taskIdSchema.parse(repair.id);
|
|
1762
|
+
const record = await driver.get("tasks", repairTaskId);
|
|
1763
|
+
if (record === null) {
|
|
1764
|
+
values.delete(repairTaskId);
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
const canonical = storedTaskSchema.parse(record.value);
|
|
1768
|
+
if (canonical.replyTo.channel === replyTo.channel && canonical.replyTo.address === replyTo.address) values.set(canonical.id, canonical);
|
|
1769
|
+
else values.delete(canonical.id);
|
|
1770
|
+
await projectSafely(() => projectTaskRoute(canonical, record.version));
|
|
1771
|
+
});
|
|
1772
|
+
const tasks = [...values.values()];
|
|
1773
|
+
for (const task of tasks) validateTaskWork(task);
|
|
1774
|
+
return tasks;
|
|
1775
|
+
},
|
|
1776
|
+
async list() {
|
|
1777
|
+
const tasks = await catalogValues("tasks", "taskCatalogs", "tasks", storedTaskSchema, taskCatalogSchema, projectTaskCatalog);
|
|
1778
|
+
for (const task of tasks) validateTaskWork(task);
|
|
1779
|
+
return tasks;
|
|
1780
|
+
}
|
|
1781
|
+
},
|
|
1782
|
+
changes: {
|
|
1783
|
+
async create(input) {
|
|
1784
|
+
const timestamp = now();
|
|
1785
|
+
const id = derivedId("chg", `${input.repositoryId}:github-pr:${input.prNumber}`);
|
|
1786
|
+
const change = changeSchema.parse({
|
|
1787
|
+
schemaVersion: 1,
|
|
1788
|
+
id,
|
|
1789
|
+
repositoryId: input.repositoryId,
|
|
1790
|
+
taskId: input.taskId,
|
|
1791
|
+
groupId: input.groupId,
|
|
1792
|
+
prNumber: input.prNumber,
|
|
1793
|
+
prUrl: input.prUrl,
|
|
1794
|
+
author: input.author,
|
|
1795
|
+
risk: input.risk,
|
|
1796
|
+
confidence: input.confidence,
|
|
1797
|
+
evidence: input.evidence,
|
|
1798
|
+
state: changeInitialState,
|
|
1799
|
+
createdAt: timestamp,
|
|
1800
|
+
updatedAt: timestamp
|
|
1801
|
+
});
|
|
1802
|
+
await mustRead("repositories", change.repositoryId, repositorySchema);
|
|
1803
|
+
if (change.taskId !== void 0) {
|
|
1804
|
+
const task = (await mustRead("tasks", change.taskId, storedTaskSchema)).value;
|
|
1805
|
+
const taskRepositoryId = task.repositoryIds[0];
|
|
1806
|
+
if (!task.repositoryIds.includes(change.repositoryId)) throw new ChangeAssociationError(change.taskId, change.repositoryId, `repository "${change.repositoryId}" does not match task repository "${taskRepositoryId}"`);
|
|
1807
|
+
}
|
|
1808
|
+
await markChangeCatalogRepair(change.id, 1);
|
|
1809
|
+
try {
|
|
1810
|
+
await driver.insert({
|
|
1811
|
+
collection: "changes",
|
|
1812
|
+
id: change.id,
|
|
1813
|
+
value: change
|
|
1814
|
+
});
|
|
1815
|
+
await projectSafely(() => projectChangeCatalog(change, 1));
|
|
1816
|
+
return change;
|
|
1817
|
+
} catch (error) {
|
|
1818
|
+
if (error instanceof RecordExistsError) {
|
|
1819
|
+
const existing = await mustRead("changes", change.id, changeSchema);
|
|
1820
|
+
await projectSafely(() => projectChangeCatalog(existing.value, existing.version));
|
|
1821
|
+
return existing.value;
|
|
1822
|
+
}
|
|
1823
|
+
throw error;
|
|
1824
|
+
}
|
|
1825
|
+
},
|
|
1826
|
+
async get(id) {
|
|
1827
|
+
const record = await read("changes", id, changeSchema);
|
|
1828
|
+
return record === null ? null : record.value;
|
|
1829
|
+
},
|
|
1830
|
+
async transition(id, to, ...args) {
|
|
1831
|
+
const transitionOptions = args[0] ?? {};
|
|
1832
|
+
const existing = await mustRead("changes", id, changeSchema);
|
|
1833
|
+
await projectSafely(() => projectChangeCatalog(existing.value, existing.version));
|
|
1834
|
+
assertTransition(changeTransitions, {
|
|
1835
|
+
from: existing.value.state,
|
|
1836
|
+
to
|
|
1837
|
+
});
|
|
1838
|
+
const invalidated = existing.value.state === "ready" && to === "verifying";
|
|
1839
|
+
const next = changeSchema.parse({
|
|
1840
|
+
...existing.value,
|
|
1841
|
+
...transitionOptions.patch,
|
|
1842
|
+
...invalidated ? {
|
|
1843
|
+
confidence: "low",
|
|
1844
|
+
evidence: unverifiedEvidence
|
|
1845
|
+
} : {},
|
|
1846
|
+
state: to,
|
|
1847
|
+
updatedAt: now()
|
|
1848
|
+
});
|
|
1849
|
+
const receiptState = changeReceiptStates[to];
|
|
1850
|
+
if (receiptState !== void 0) await appendReceipt(`change:${id}:${receiptState}`, {
|
|
1851
|
+
repositoryIds: [next.repositoryId],
|
|
1852
|
+
taskId: next.taskId,
|
|
1853
|
+
changeId: id,
|
|
1854
|
+
kind: "change",
|
|
1855
|
+
state: receiptState,
|
|
1856
|
+
reason: transitionOptions.reason,
|
|
1857
|
+
attempt: 1,
|
|
1858
|
+
decidedBy: next.decidedBy
|
|
1859
|
+
});
|
|
1860
|
+
await updateChange(next, existing.version);
|
|
1861
|
+
return next;
|
|
1862
|
+
},
|
|
1863
|
+
list() {
|
|
1864
|
+
return catalogValues("changes", "changeCatalogs", "changes", changeSchema, changeCatalogSchema, projectChangeCatalog);
|
|
1865
|
+
}
|
|
1866
|
+
},
|
|
1867
|
+
receipts: {
|
|
1868
|
+
append(input) {
|
|
1869
|
+
const { dedupeKey, attempt, ...fields } = input;
|
|
1870
|
+
return insertReceipt(dedupeKey === void 0 ? randomId("rcpt") : derivedId("rcpt", `custom:${dedupeKey}`), {
|
|
1871
|
+
...fields,
|
|
1872
|
+
attempt: attempt ?? 1
|
|
1873
|
+
});
|
|
1874
|
+
},
|
|
1875
|
+
async get(id) {
|
|
1876
|
+
const record = await read("receipts", id, receiptSchema);
|
|
1877
|
+
return record === null ? null : record.value;
|
|
1878
|
+
},
|
|
1879
|
+
list() {
|
|
1880
|
+
return listAll("receipts", receiptSchema);
|
|
1881
|
+
}
|
|
1882
|
+
},
|
|
1883
|
+
budgets: {
|
|
1884
|
+
async ensure(input) {
|
|
1885
|
+
const budget = budgetSchema.parse({
|
|
1886
|
+
schemaVersion: 1,
|
|
1887
|
+
factoryId: input.factoryId,
|
|
1888
|
+
month: input.month,
|
|
1889
|
+
limits: input.limits,
|
|
1890
|
+
notices: {}
|
|
1891
|
+
});
|
|
1892
|
+
try {
|
|
1893
|
+
await driver.insert({
|
|
1894
|
+
collection: "budgets",
|
|
1895
|
+
id: input.month,
|
|
1896
|
+
value: budget
|
|
1897
|
+
});
|
|
1898
|
+
return budget;
|
|
1899
|
+
} catch (error) {
|
|
1900
|
+
if (!(error instanceof RecordExistsError)) throw error;
|
|
1901
|
+
}
|
|
1902
|
+
const existing = await mustRead("budgets", input.month, budgetSchema);
|
|
1903
|
+
const refreshed = budgetSchema.parse({
|
|
1904
|
+
...existing.value,
|
|
1905
|
+
limits: input.limits
|
|
1906
|
+
});
|
|
1907
|
+
if (JSON.stringify(refreshed) === JSON.stringify(existing.value)) return existing.value;
|
|
1908
|
+
try {
|
|
1909
|
+
await driver.update({
|
|
1910
|
+
collection: "budgets",
|
|
1911
|
+
id: input.month,
|
|
1912
|
+
value: refreshed,
|
|
1913
|
+
expectedVersion: existing.version
|
|
1914
|
+
});
|
|
1915
|
+
return refreshed;
|
|
1916
|
+
} catch (error) {
|
|
1917
|
+
if (error instanceof VersionConflictError) return (await mustRead("budgets", input.month, budgetSchema)).value;
|
|
1918
|
+
throw error;
|
|
1919
|
+
}
|
|
1920
|
+
},
|
|
1921
|
+
async get(month) {
|
|
1922
|
+
const record = await read("budgets", month, budgetSchema);
|
|
1923
|
+
return record === null ? null : record.value;
|
|
1924
|
+
},
|
|
1925
|
+
async stampNotice(month, notice) {
|
|
1926
|
+
const field = notice === "warning" ? "warningAt" : "exhaustedAt";
|
|
1927
|
+
for (;;) {
|
|
1928
|
+
const existing = await mustRead("budgets", month, budgetSchema);
|
|
1929
|
+
if (existing.value.notices[field] !== void 0) return {
|
|
1930
|
+
budget: existing.value,
|
|
1931
|
+
stamped: false
|
|
1932
|
+
};
|
|
1933
|
+
const next = budgetSchema.parse({
|
|
1934
|
+
...existing.value,
|
|
1935
|
+
notices: {
|
|
1936
|
+
...existing.value.notices,
|
|
1937
|
+
[field]: now()
|
|
1938
|
+
}
|
|
1939
|
+
});
|
|
1940
|
+
try {
|
|
1941
|
+
await driver.update({
|
|
1942
|
+
collection: "budgets",
|
|
1943
|
+
id: month,
|
|
1944
|
+
value: next,
|
|
1945
|
+
expectedVersion: existing.version
|
|
1946
|
+
});
|
|
1947
|
+
return {
|
|
1948
|
+
budget: next,
|
|
1949
|
+
stamped: true
|
|
1950
|
+
};
|
|
1951
|
+
} catch (error) {
|
|
1952
|
+
if (!(error instanceof VersionConflictError)) throw error;
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
},
|
|
1957
|
+
answers: { async get(id) {
|
|
1958
|
+
return (await read("answers", id, answerEventSchema))?.value ?? null;
|
|
1959
|
+
} },
|
|
1960
|
+
outbox: {
|
|
1961
|
+
async enqueue(input) {
|
|
1962
|
+
const event = communicationEventSchema.parse(input);
|
|
1963
|
+
const id = outboxIdSchema.parse(derivedId("out", event.eventId));
|
|
1964
|
+
const delivered = await read("outboxDeliveries", id, outboxMessageSchema);
|
|
1965
|
+
if (delivered !== null) {
|
|
1966
|
+
if (!isDeepStrictEqual(delivered.value.event, event)) throw new OutboxEventConflictError(event.eventId);
|
|
1967
|
+
return delivered.value;
|
|
1968
|
+
}
|
|
1969
|
+
const queueId = `communication-${id.slice(4, 5)}`;
|
|
1970
|
+
for (;;) {
|
|
1971
|
+
const existing = await read("outboxQueues", queueId, outboxQueueSchema);
|
|
1972
|
+
const duplicate = existing?.value.messages.find((message) => message.id === id);
|
|
1973
|
+
if (duplicate !== void 0) {
|
|
1974
|
+
if (!isDeepStrictEqual(duplicate.event, event)) throw new OutboxEventConflictError(event.eventId);
|
|
1975
|
+
return duplicate;
|
|
1976
|
+
}
|
|
1977
|
+
const timestamp = now();
|
|
1978
|
+
const message = outboxMessageSchema.parse({
|
|
1979
|
+
schemaVersion: 1,
|
|
1980
|
+
id,
|
|
1981
|
+
event,
|
|
1982
|
+
state: "pending",
|
|
1983
|
+
attempt: 0,
|
|
1984
|
+
availableAt: timestamp,
|
|
1985
|
+
createdAt: timestamp,
|
|
1986
|
+
updatedAt: timestamp
|
|
1987
|
+
});
|
|
1988
|
+
const queue = outboxQueueSchema.parse({
|
|
1989
|
+
schemaVersion: 1,
|
|
1990
|
+
id: queueId,
|
|
1991
|
+
messages: [...existing?.value.messages ?? [], message],
|
|
1992
|
+
updatedAt: timestamp
|
|
1993
|
+
});
|
|
1994
|
+
try {
|
|
1995
|
+
if (existing === null) await driver.insert({
|
|
1996
|
+
collection: "outboxQueues",
|
|
1997
|
+
id: queueId,
|
|
1998
|
+
value: queue
|
|
1999
|
+
});
|
|
2000
|
+
else await driver.update({
|
|
2001
|
+
collection: "outboxQueues",
|
|
2002
|
+
id: queueId,
|
|
2003
|
+
value: queue,
|
|
2004
|
+
expectedVersion: existing.version
|
|
2005
|
+
});
|
|
2006
|
+
return message;
|
|
2007
|
+
} catch (error) {
|
|
2008
|
+
if (!(error instanceof RecordExistsError) && !(error instanceof VersionConflictError)) throw error;
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
},
|
|
2012
|
+
async get(id) {
|
|
2013
|
+
const delivered = await read("outboxDeliveries", id, outboxMessageSchema);
|
|
2014
|
+
if (delivered !== null) return delivered.value;
|
|
2015
|
+
return (await read("outboxQueues", `communication-${id.slice(4, 5)}`, outboxQueueSchema))?.value.messages.find((message) => message.id === id) ?? null;
|
|
2016
|
+
},
|
|
2017
|
+
async claim(id, leaseMs = 6e4) {
|
|
2018
|
+
if (!Number.isSafeInteger(leaseMs) || leaseMs <= 0) throw new Error("outbox leaseMs must be a positive safe integer");
|
|
2019
|
+
const queueId = `communication-${id.slice(4, 5)}`;
|
|
2020
|
+
for (;;) {
|
|
2021
|
+
const queue = await mustRead("outboxQueues", queueId, outboxQueueSchema);
|
|
2022
|
+
const index = queue.value.messages.findIndex((message) => message.id === id);
|
|
2023
|
+
if (index < 0) return null;
|
|
2024
|
+
if (await read("outboxDeliveries", id, outboxMessageSchema) !== null) {
|
|
2025
|
+
const messages = queue.value.messages.filter((message) => message.id !== id);
|
|
2026
|
+
try {
|
|
2027
|
+
await driver.update({
|
|
2028
|
+
collection: "outboxQueues",
|
|
2029
|
+
id: queueId,
|
|
2030
|
+
value: outboxQueueSchema.parse({
|
|
2031
|
+
...queue.value,
|
|
2032
|
+
messages,
|
|
2033
|
+
updatedAt: now()
|
|
2034
|
+
}),
|
|
2035
|
+
expectedVersion: queue.version
|
|
2036
|
+
});
|
|
2037
|
+
return null;
|
|
2038
|
+
} catch (error) {
|
|
2039
|
+
if (!(error instanceof VersionConflictError)) throw error;
|
|
2040
|
+
continue;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
const message = queue.value.messages[index];
|
|
2044
|
+
const timestamp = now();
|
|
2045
|
+
if (!(message.state === "pending" && message.availableAt <= timestamp || message.state === "delivering" && message.leaseUntil !== void 0 && message.leaseUntil <= timestamp)) return null;
|
|
2046
|
+
const next = outboxMessageSchema.parse({
|
|
2047
|
+
...message,
|
|
2048
|
+
state: "delivering",
|
|
2049
|
+
attempt: message.attempt + 1,
|
|
2050
|
+
leaseUntil: new Date(Date.parse(timestamp) + leaseMs).toISOString(),
|
|
2051
|
+
updatedAt: timestamp
|
|
2052
|
+
});
|
|
2053
|
+
const messages = [...queue.value.messages];
|
|
2054
|
+
messages[index] = next;
|
|
2055
|
+
try {
|
|
2056
|
+
await driver.update({
|
|
2057
|
+
collection: "outboxQueues",
|
|
2058
|
+
id: queueId,
|
|
2059
|
+
value: outboxQueueSchema.parse({
|
|
2060
|
+
...queue.value,
|
|
2061
|
+
messages,
|
|
2062
|
+
updatedAt: timestamp
|
|
2063
|
+
}),
|
|
2064
|
+
expectedVersion: queue.version
|
|
2065
|
+
});
|
|
2066
|
+
return next;
|
|
2067
|
+
} catch (error) {
|
|
2068
|
+
if (!(error instanceof VersionConflictError)) throw error;
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
},
|
|
2072
|
+
async retry(id, input) {
|
|
2073
|
+
const { attempt, reason, delayMs } = input;
|
|
2074
|
+
if (!Number.isSafeInteger(delayMs) || delayMs < 0) throw new Error("outbox delayMs must be a nonnegative safe integer");
|
|
2075
|
+
const queueId = `communication-${id.slice(4, 5)}`;
|
|
2076
|
+
for (;;) {
|
|
2077
|
+
const queue = await mustRead("outboxQueues", queueId, outboxQueueSchema);
|
|
2078
|
+
const index = queue.value.messages.findIndex((message) => message.id === id);
|
|
2079
|
+
if (index < 0) return null;
|
|
2080
|
+
const message = queue.value.messages[index];
|
|
2081
|
+
if (message.state !== "delivering" || message.attempt !== attempt) return null;
|
|
2082
|
+
const timestamp = now();
|
|
2083
|
+
const { leaseUntil: _leaseUntil, ...leased } = message;
|
|
2084
|
+
const next = outboxMessageSchema.parse({
|
|
2085
|
+
...leased,
|
|
2086
|
+
state: "pending",
|
|
2087
|
+
availableAt: new Date(Date.parse(timestamp) + delayMs).toISOString(),
|
|
2088
|
+
lastError: reason.trim().slice(0, 2e3) || "delivery failed",
|
|
2089
|
+
updatedAt: timestamp
|
|
2090
|
+
});
|
|
2091
|
+
const messages = [...queue.value.messages];
|
|
2092
|
+
messages[index] = next;
|
|
2093
|
+
try {
|
|
2094
|
+
await driver.update({
|
|
2095
|
+
collection: "outboxQueues",
|
|
2096
|
+
id: queueId,
|
|
2097
|
+
value: outboxQueueSchema.parse({
|
|
2098
|
+
...queue.value,
|
|
2099
|
+
messages,
|
|
2100
|
+
updatedAt: timestamp
|
|
2101
|
+
}),
|
|
2102
|
+
expectedVersion: queue.version
|
|
2103
|
+
});
|
|
2104
|
+
return next;
|
|
2105
|
+
} catch (error) {
|
|
2106
|
+
if (!(error instanceof VersionConflictError)) throw error;
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
},
|
|
2110
|
+
async complete(id, attempt) {
|
|
2111
|
+
const queueId = `communication-${id.slice(4, 5)}`;
|
|
2112
|
+
let delivered = await read("outboxDeliveries", id, outboxMessageSchema);
|
|
2113
|
+
for (;;) {
|
|
2114
|
+
const queue = await mustRead("outboxQueues", queueId, outboxQueueSchema);
|
|
2115
|
+
const index = queue.value.messages.findIndex((message) => message.id === id);
|
|
2116
|
+
if (index < 0) return delivered?.value ?? null;
|
|
2117
|
+
const message = queue.value.messages[index];
|
|
2118
|
+
const timestamp = now();
|
|
2119
|
+
if (delivered === null) {
|
|
2120
|
+
if (message.state !== "delivering" || message.attempt !== attempt) return null;
|
|
2121
|
+
const { leaseUntil: _leaseUntil, lastError: _lastError, ...leased } = message;
|
|
2122
|
+
const next = outboxMessageSchema.parse({
|
|
2123
|
+
...leased,
|
|
2124
|
+
state: "delivered",
|
|
2125
|
+
deliveredAt: timestamp,
|
|
2126
|
+
updatedAt: timestamp
|
|
2127
|
+
});
|
|
2128
|
+
try {
|
|
2129
|
+
await driver.insert({
|
|
2130
|
+
collection: "outboxDeliveries",
|
|
2131
|
+
id,
|
|
2132
|
+
value: next
|
|
2133
|
+
});
|
|
2134
|
+
delivered = {
|
|
2135
|
+
value: next,
|
|
2136
|
+
version: 1
|
|
2137
|
+
};
|
|
2138
|
+
} catch (error) {
|
|
2139
|
+
if (!(error instanceof RecordExistsError)) throw error;
|
|
2140
|
+
delivered = await mustRead("outboxDeliveries", id, outboxMessageSchema);
|
|
2141
|
+
if (!isDeepStrictEqual(delivered.value.event, message.event)) throw new OutboxEventConflictError(message.event.eventId);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
const messages = queue.value.messages.filter((candidate) => candidate.id !== id);
|
|
2145
|
+
try {
|
|
2146
|
+
await driver.update({
|
|
2147
|
+
collection: "outboxQueues",
|
|
2148
|
+
id: queueId,
|
|
2149
|
+
value: outboxQueueSchema.parse({
|
|
2150
|
+
...queue.value,
|
|
2151
|
+
messages,
|
|
2152
|
+
updatedAt: timestamp
|
|
2153
|
+
}),
|
|
2154
|
+
expectedVersion: queue.version
|
|
2155
|
+
});
|
|
2156
|
+
return delivered.value;
|
|
2157
|
+
} catch (error) {
|
|
2158
|
+
if (!(error instanceof VersionConflictError)) throw error;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
},
|
|
2162
|
+
async pending(limit = 100) {
|
|
2163
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) throw new Error("outbox pending limit must be a positive safe integer");
|
|
2164
|
+
const queues = await Promise.all("0123456789abcdef".split("").map((shard) => read("outboxQueues", `communication-${shard}`, outboxQueueSchema)));
|
|
2165
|
+
const timestamp = now();
|
|
2166
|
+
return queues.flatMap((queue) => queue?.value.messages ?? []).filter((message) => message.state === "pending" && message.availableAt <= timestamp || message.state === "delivering" && message.leaseUntil !== void 0 && message.leaseUntil <= timestamp).sort((a, b) => a.createdAt.localeCompare(b.createdAt)).slice(0, limit);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
};
|
|
2170
|
+
return stores;
|
|
2171
|
+
}
|
|
2172
|
+
//#endregion
|
|
2173
|
+
export { AnswerEventConflictError, ApprovalError, ChangeAssociationError, ImageInputError, OutboxEventConflictError, SessionConflictError, SettlementError, createStores };
|
|
2174
|
+
|
|
2175
|
+
//# sourceMappingURL=engine.mjs.map
|