@wardby/cli 0.5.3 → 0.6.0
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/.env.example +33 -0
- package/README.md +25 -14
- package/dist/claude-coding-worker/context.d.ts +22 -0
- package/dist/claude-coding-worker/context.js +92 -0
- package/dist/claude-coding-worker/driver.d.ts +7 -1
- package/dist/claude-coding-worker/driver.js +38 -5
- package/dist/claude-coding-worker/sdk.js +13 -4
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +5 -2
- package/dist/cli-native-mode.d.ts +9 -0
- package/dist/cli-native-mode.js +20 -0
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +141 -5
- package/dist/coding/claude-context.d.ts +45 -0
- package/dist/coding/claude-context.js +434 -0
- package/dist/coding/profile.d.ts +12 -0
- package/dist/coding/profile.js +6 -0
- package/dist/coding/protocol.d.ts +104 -0
- package/dist/coding/protocol.js +87 -3
- package/dist/coding-worker/artifact.d.ts +8 -0
- package/dist/coding-worker/artifact.js +2 -2
- package/dist/coding-worker/driver.js +2 -0
- package/dist/coding-worker/sdk.d.ts +6 -0
- package/dist/coding-worker/sdk.js +1 -0
- package/dist/coding-worker/skills.d.ts +23 -0
- package/dist/coding-worker/skills.js +117 -0
- package/dist/coding-worker/types.d.ts +2 -0
- package/dist/config/github-allowed-accounts.d.ts +15 -0
- package/dist/config/github-allowed-accounts.js +36 -0
- package/dist/config/low-power.d.ts +12 -0
- package/dist/config/low-power.js +18 -0
- package/dist/config/mobile.d.ts +12 -0
- package/dist/config/mobile.js +18 -0
- package/dist/config/providers.d.ts +53 -0
- package/dist/config/providers.js +92 -6
- package/dist/core/db.d.ts +16 -6
- package/dist/core/db.js +12 -10
- package/dist/core/dispatch.d.ts +11 -0
- package/dist/core/dispatch.js +36 -1
- package/dist/core/engine-native.js +9 -5
- package/dist/core/host-events.d.ts +49 -3
- package/dist/core/host-events.js +289 -15
- package/dist/core/issue-bridge.js +42 -2
- package/dist/core/issue-events.js +12 -1
- package/dist/core/issue-status.js +14 -3
- package/dist/core/merge-order-check.d.ts +129 -0
- package/dist/core/merge-order-check.js +336 -0
- package/dist/core/notification-dispatcher.d.ts +70 -0
- package/dist/core/notification-dispatcher.js +368 -0
- package/dist/core/notification-templates.d.ts +25 -0
- package/dist/core/notification-templates.js +105 -0
- package/dist/core/notifications.d.ts +10 -0
- package/dist/core/notifications.js +38 -0
- package/dist/core/pg-auth.d.ts +22 -0
- package/dist/core/pg-auth.js +152 -0
- package/dist/core/power-composition.d.ts +16 -0
- package/dist/core/power-composition.js +37 -0
- package/dist/core/power-idle.d.ts +14 -0
- package/dist/core/power-idle.js +30 -0
- package/dist/core/power.d.ts +71 -0
- package/dist/core/power.js +202 -0
- package/dist/core/reconciler.d.ts +4 -2
- package/dist/core/reconciler.js +29 -5
- package/dist/core/related-pull-requests.d.ts +48 -4
- package/dist/core/related-pull-requests.js +113 -11
- package/dist/core/repo-access.d.ts +15 -0
- package/dist/core/repo-access.js +66 -0
- package/dist/core/review-fix.d.ts +37 -0
- package/dist/core/review-fix.js +207 -6
- package/dist/core/review-host-tools.d.ts +20 -0
- package/dist/core/review-host-tools.js +58 -2
- package/dist/core/runner.d.ts +336 -3
- package/dist/core/runner.js +894 -317
- package/dist/core/secret-broker-config.d.ts +103 -0
- package/dist/core/secret-broker-config.js +112 -0
- package/dist/core/secrets.d.ts +39 -4
- package/dist/core/secrets.js +148 -40
- package/dist/core/workflow-event-recorder.d.ts +14 -0
- package/dist/core/workflow-event-recorder.js +120 -0
- package/dist/core/workflow-events.d.ts +82 -0
- package/dist/core/workflow-events.js +44 -0
- package/dist/core/workflow-run-events.d.ts +16 -0
- package/dist/core/workflow-run-events.js +40 -0
- package/dist/generated/prisma/browser.d.ts +58 -0
- package/dist/generated/prisma/client.d.ts +58 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +150 -0
- package/dist/generated/prisma/enums.d.ts +27 -0
- package/dist/generated/prisma/enums.js +23 -0
- package/dist/generated/prisma/internal/class.d.ts +88 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +772 -1
- package/dist/generated/prisma/internal/prismaNamespace.js +135 -4
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +139 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +135 -4
- package/dist/generated/prisma/models/Agent.d.ts +360 -1
- package/dist/generated/prisma/models/CodingAgentProfile.d.ts +73 -1
- package/dist/generated/prisma/models/CodingRun.d.ts +205 -1
- package/dist/generated/prisma/models/DeferredReview.d.ts +78 -1
- package/dist/generated/prisma/models/NativeGatewayCall.d.ts +1675 -0
- package/dist/generated/prisma/models/NativeGatewayCall.js +1 -0
- package/dist/generated/prisma/models/NativeGatewaySession.d.ts +1517 -0
- package/dist/generated/prisma/models/NativeGatewaySession.js +1 -0
- package/dist/generated/prisma/models/NativeWarmWorker.d.ts +1296 -0
- package/dist/generated/prisma/models/NativeWarmWorker.js +1 -0
- package/dist/generated/prisma/models/NotificationChannel.d.ts +1543 -0
- package/dist/generated/prisma/models/NotificationChannel.js +1 -0
- package/dist/generated/prisma/models/NotificationDelivery.d.ts +1502 -0
- package/dist/generated/prisma/models/NotificationDelivery.js +1 -0
- package/dist/generated/prisma/models/NotificationThread.d.ts +1090 -0
- package/dist/generated/prisma/models/NotificationThread.js +1 -0
- package/dist/generated/prisma/models/Run.d.ts +826 -1
- package/dist/generated/prisma/models/RunHostCheck.d.ts +36 -1
- package/dist/generated/prisma/models/Secret.d.ts +37 -1
- package/dist/generated/prisma/models/SecretBrokerChange.d.ts +1101 -0
- package/dist/generated/prisma/models/SecretBrokerChange.js +1 -0
- package/dist/generated/prisma/models/WorkflowEvent.d.ts +1422 -0
- package/dist/generated/prisma/models/WorkflowEvent.js +1 -0
- package/dist/generated/prisma/models.d.ts +8 -0
- package/dist/help/build.js +4 -2
- package/dist/help/catalog.d.ts +1 -1
- package/dist/help/catalog.js +31 -6
- package/dist/help-index.json +1363 -176
- package/dist/import/create.js +17 -0
- package/dist/import/neutral-schema.d.ts +6 -2
- package/dist/import/neutral-schema.js +2 -0
- package/dist/mcp/auth/access.d.ts +1 -0
- package/dist/mcp/auth/repo-authorization.js +14 -0
- package/dist/mcp/auth/resource-server.d.ts +2 -1
- package/dist/mcp/auth/resource-server.js +7 -2
- package/dist/mcp/auth/self-hosted/browser.d.ts +1 -1
- package/dist/mcp/auth/self-hosted/browser.js +19 -6
- package/dist/mcp/context.d.ts +6 -0
- package/dist/mcp/host-events/github-ingress.d.ts +23 -1
- package/dist/mcp/host-events/github-ingress.js +68 -0
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/index.js +59 -10
- package/dist/mcp/tools/agents.js +82 -11
- package/dist/mcp/tools/notification-channels.d.ts +2 -0
- package/dist/mcp/tools/notification-channels.js +293 -0
- package/dist/mcp/tools/power.d.ts +2 -0
- package/dist/mcp/tools/power.js +77 -0
- package/dist/mcp/tools/runs.d.ts +0 -6
- package/dist/mcp/tools/runs.js +7 -2
- package/dist/mcp/tools/secret-broker-compat.d.ts +12 -0
- package/dist/mcp/tools/secret-broker-compat.js +23 -0
- package/dist/mcp/tools/secret-elicitation-form.d.ts +22 -2
- package/dist/mcp/tools/secret-elicitation-form.js +200 -10
- package/dist/mcp/tools/secret-elicitation-server.js +8 -1
- package/dist/mcp/tools/secret-elicitation.d.ts +38 -3
- package/dist/mcp/tools/secret-elicitation.js +110 -12
- package/dist/mcp/tools/secrets.js +184 -7
- package/dist/mcp/tools/tools.d.ts +1 -1
- package/dist/mcp/tools/tools.js +22 -2
- package/dist/native-worker/composition.d.ts +23 -0
- package/dist/native-worker/composition.js +77 -0
- package/dist/native-worker/docker-isolation.d.ts +65 -0
- package/dist/native-worker/docker-isolation.js +183 -0
- package/dist/native-worker/docker-launcher.d.ts +74 -0
- package/dist/native-worker/docker-launcher.js +237 -0
- package/dist/native-worker/gateway-server.d.ts +39 -0
- package/dist/native-worker/gateway-server.js +315 -0
- package/dist/native-worker/gateway.d.ts +44 -0
- package/dist/native-worker/gateway.js +197 -0
- package/dist/native-worker/http-server.d.ts +22 -0
- package/dist/native-worker/http-server.js +109 -0
- package/dist/native-worker/http-transport.d.ts +17 -0
- package/dist/native-worker/http-transport.js +88 -0
- package/dist/native-worker/kubernetes-isolation.d.ts +90 -0
- package/dist/native-worker/kubernetes-isolation.js +218 -0
- package/dist/native-worker/kubernetes-launcher.d.ts +102 -0
- package/dist/native-worker/kubernetes-launcher.js +335 -0
- package/dist/native-worker/launch.d.ts +45 -0
- package/dist/native-worker/launch.js +44 -0
- package/dist/native-worker/ledger.d.ts +109 -0
- package/dist/native-worker/ledger.js +192 -0
- package/dist/native-worker/loopback.d.ts +12 -0
- package/dist/native-worker/loopback.js +49 -0
- package/dist/native-worker/main.d.ts +7 -0
- package/dist/native-worker/main.js +74 -0
- package/dist/native-worker/protocol.d.ts +744 -0
- package/dist/native-worker/protocol.js +200 -0
- package/dist/native-worker/sandbox-executor.d.ts +68 -0
- package/dist/native-worker/sandbox-executor.js +210 -0
- package/dist/native-worker/stdio.d.ts +29 -0
- package/dist/native-worker/stdio.js +188 -0
- package/dist/native-worker/warm-delivery.d.ts +16 -0
- package/dist/native-worker/warm-delivery.js +33 -0
- package/dist/native-worker/warm-pool-ledger.d.ts +39 -0
- package/dist/native-worker/warm-pool-ledger.js +73 -0
- package/dist/native-worker/warm-pool.d.ts +107 -0
- package/dist/native-worker/warm-pool.js +283 -0
- package/dist/native-worker/worker.d.ts +17 -0
- package/dist/native-worker/worker.js +115 -0
- package/dist/providers/auth/authorization-server.d.ts +3 -0
- package/dist/providers/auth/index.js +2 -0
- package/dist/providers/auth/self-hosted.d.ts +13 -1
- package/dist/providers/auth/self-hosted.js +54 -4
- package/dist/providers/chat/fake.d.ts +46 -0
- package/dist/providers/chat/fake.js +39 -0
- package/dist/providers/chat/index.d.ts +4 -0
- package/dist/providers/chat/index.js +8 -0
- package/dist/providers/chat/slack.d.ts +37 -0
- package/dist/providers/chat/slack.js +105 -0
- package/dist/providers/chat/types.d.ts +42 -0
- package/dist/providers/chat/types.js +19 -0
- package/dist/providers/engine/types.d.ts +19 -0
- package/dist/providers/engine/types.js +22 -0
- package/dist/providers/executor/composition.d.ts +24 -1
- package/dist/providers/executor/composition.js +39 -6
- package/dist/providers/executor/container.d.ts +24 -1
- package/dist/providers/executor/container.js +101 -11
- package/dist/providers/executor/dbos.d.ts +42 -0
- package/dist/providers/executor/dbos.js +118 -12
- package/dist/providers/executor/deferred.d.ts +56 -0
- package/dist/providers/executor/deferred.js +119 -0
- package/dist/providers/executor/routing.d.ts +37 -10
- package/dist/providers/executor/routing.js +73 -32
- package/dist/providers/executor/types.d.ts +12 -0
- package/dist/providers/jobs/docker.js +3 -3
- package/dist/providers/jobs/fake-kubernetes-api.d.ts +21 -2
- package/dist/providers/jobs/fake-kubernetes-api.js +42 -1
- package/dist/providers/jobs/kubernetes-api.d.ts +17 -1
- package/dist/providers/jobs/kubernetes-api.js +7 -0
- package/dist/providers/jobs/kubernetes-client.d.ts +10 -1
- package/dist/providers/jobs/kubernetes-client.js +52 -3
- package/dist/providers/jobs/kubernetes-dry-run-fixture.d.ts +6 -1
- package/dist/providers/jobs/kubernetes-dry-run-fixture.js +6 -3
- package/dist/providers/jobs/kubernetes-isolation.d.ts +3 -1
- package/dist/providers/jobs/kubernetes-isolation.js +6 -0
- package/dist/providers/jobs/kubernetes-platform.d.ts +4 -1
- package/dist/providers/jobs/kubernetes-platform.js +38 -1
- package/dist/providers/jobs/kubernetes-preflight.d.ts +2 -1
- package/dist/providers/jobs/kubernetes-preflight.js +4 -2
- package/dist/providers/jobs/kubernetes.d.ts +36 -1
- package/dist/providers/jobs/kubernetes.js +74 -4
- package/dist/providers/jobs/metadata-probes.d.ts +11 -0
- package/dist/providers/jobs/metadata-probes.js +13 -0
- package/dist/providers/jobs/types.d.ts +9 -0
- package/dist/providers/review-host/github.d.ts +11 -1
- package/dist/providers/review-host/github.js +50 -0
- package/dist/providers/review-host/types.d.ts +23 -0
- package/dist/providers/vcs/github.d.ts +32 -6
- package/dist/providers/vcs/github.js +74 -16
- package/dist/quickstart/coding-seed.d.ts +10 -3
- package/dist/quickstart/coding-seed.js +74 -3
- package/dist/quickstart/images.d.ts +2 -0
- package/dist/quickstart/images.js +7 -1
- package/dist/quickstart/reviewer-prompt.d.ts +1 -1
- package/dist/quickstart/reviewer-prompt.js +1 -1
- package/dist/quickstart-images.json +1 -1
- package/dist/sandbox/brokered-fetch.d.ts +23 -0
- package/dist/sandbox/brokered-fetch.js +37 -0
- package/dist/sandbox/host-functions.d.ts +34 -0
- package/dist/sandbox/host-functions.js +144 -80
- package/dist/sandbox/prelude.js +7 -1
- package/dist/sandbox/run-in-sandbox.d.ts +19 -0
- package/dist/sandbox/run-in-sandbox.js +28 -10
- package/dist/sandbox/safe-fetch.d.ts +2 -0
- package/dist/sandbox/safe-fetch.js +3 -1
- package/dist/sandbox/secret-broker-sigv4.d.ts +2 -0
- package/dist/sandbox/secret-broker-sigv4.js +49 -0
- package/dist/sandbox/secret-broker.d.ts +34 -0
- package/dist/sandbox/secret-broker.js +162 -0
- package/dist/sandbox/user-tool.d.ts +14 -0
- package/dist/sandbox/user-tool.js +32 -0
- package/dist/serve.js +18 -0
- package/dist/viewer/api-schema.d.ts +210 -14
- package/dist/viewer/api-schema.js +22 -0
- package/dist/viewer/build-schemas.js +4 -1
- package/dist/viewer/event-bus.d.ts +6 -0
- package/dist/viewer/event-bus.js +16 -7
- package/dist/viewer/graph.d.ts +5 -2
- package/dist/viewer/graph.js +7 -2
- package/dist/viewer/http.d.ts +0 -1
- package/dist/viewer/http.js +2 -2
- package/dist/viewer/infra.js +25 -2
- package/dist/viewer/limits.d.ts +4 -0
- package/dist/viewer/limits.js +4 -0
- package/dist/viewer/openapi.d.ts +15 -0
- package/dist/viewer/openapi.js +172 -0
- package/docs/README.md +7 -0
- package/docs/agent-recipes.md +57 -8
- package/docs/architecture-runtime.md +16 -1
- package/docs/code-review-agents.md +139 -5
- package/docs/coding-agent-setup.md +113 -5
- package/docs/coding-packages.md +4 -2
- package/docs/coding-services.md +3 -1
- package/docs/coding-worker-byo-images.md +198 -6
- package/docs/coding-worker-isolation.md +68 -14
- package/docs/getting-started-gke.md +225 -9
- package/docs/getting-started-identity-provider.md +21 -10
- package/docs/getting-started.md +5 -4
- package/docs/jira-agents.md +5 -1
- package/docs/native-sandbox.md +635 -0
- package/docs/release-verification.md +9 -0
- package/docs/security-deployment.md +113 -12
- package/docs/slack-notifications.md +252 -0
- package/docs/tool-secrets.md +252 -0
- package/docs/viewer-api.md +46 -5
- package/docs/viewer-api.openapi.yml +1404 -0
- package/help/admin-viewer.md +6 -2
- package/help/agent-recipes.md +27 -0
- package/help/architecture-agent.md +1 -1
- package/help/brokered-secrets.md +79 -0
- package/help/build-claude-code-image.md +131 -0
- package/help/build-worker-image.md +3 -6
- package/help/code-review-agents.md +68 -6
- package/help/deploy-gke.md +8 -1
- package/help/deployment-targets.md +25 -14
- package/help/errors/coding-provider-not-configured.md +2 -2
- package/help/errors/database-auth-invalid.md +28 -0
- package/help/errors/github-account-not-allowed.md +25 -0
- package/help/errors/low-power-disabled.md +29 -0
- package/help/errors/low-power-scale-forbidden.md +35 -0
- package/help/errors/low-power-wake-timeout.md +30 -0
- package/help/errors/mobile-disabled.md +31 -0
- package/help/errors/native-sandbox-capacity.md +22 -0
- package/help/errors/native-sandbox-deadline-exceeded.md +19 -0
- package/help/errors/native-sandbox-docker-failed.md +20 -0
- package/help/errors/native-sandbox-gateway-unavailable.md +21 -0
- package/help/errors/native-sandbox-image-not-pinned.md +19 -0
- package/help/errors/native-sandbox-isolation-mismatch.md +20 -0
- package/help/errors/native-sandbox-network-unenforced.md +22 -0
- package/help/errors/native-sandbox-requires-catalog.md +31 -0
- package/help/errors/native-sandbox-unavailable.md +19 -0
- package/help/errors/native-sandbox-warm-delivery-failed.md +21 -0
- package/help/errors/native-sandbox-worker-exited.md +20 -0
- package/help/errors/native-sandbox-worker-lost.md +20 -0
- package/help/errors/native-sandbox-worker-unready.md +22 -0
- package/help/errors/secret-broker-body-invalid.md +26 -0
- package/help/errors/secret-broker-changed.md +20 -0
- package/help/errors/secret-broker-config-invalid.md +30 -0
- package/help/errors/secret-broker-conflict.md +27 -0
- package/help/errors/secret-broker-destination-denied.md +28 -0
- package/help/errors/secret-broker-value-invalid.md +28 -0
- package/help/errors/secret-brokered.md +24 -0
- package/help/errors/secret-not-brokered.md +25 -0
- package/help/errors/slack-auth-failed.md +48 -0
- package/help/errors/slack-channel-unreachable.md +39 -0
- package/help/errors/slack-not-configured.md +33 -0
- package/help/getting-started.md +4 -5
- package/help/github.md +18 -1
- package/help/identity-and-access.md +5 -1
- package/help/local-repositories.md +2 -3
- package/help/low-power-mode.md +125 -0
- package/help/merge-order-check.md +117 -0
- package/help/mobile-app.md +54 -0
- package/help/native-sandbox.md +136 -0
- package/help/observability.md +8 -0
- package/help/related-pull-requests.md +70 -9
- package/help/repo-instructions-and-skills.md +91 -0
- package/help/review-fix-rounds.md +10 -0
- package/help/slack-notifications.md +62 -0
- package/help/troubleshooting/coding-workers.md +21 -0
- package/package.json +11 -4
- package/prisma/migrations/20261009000003_native_execution_mode/migration.sql +12 -0
- package/prisma/migrations/20261009000004_native_gateway_ledger/migration.sql +65 -0
- package/prisma/migrations/20261009000005_native_gateway_call_child_agent/migration.sql +5 -0
- package/prisma/migrations/20261009000006_run_deferred_start_stop/migration.sql +14 -0
- package/prisma/migrations/20261009000007_native_gateway_network_ready/migration.sql +5 -0
- package/prisma/migrations/20261010000001_workflow_notifications/migration.sql +99 -0
- package/prisma/migrations/20261011000001_native_gateway_session_status_index/migration.sql +2 -0
- package/prisma/migrations/20261012000001_native_warm_worker/migration.sql +32 -0
- package/prisma/migrations/20261013000001_coding_run_merge_order/migration.sql +2 -0
- package/prisma/migrations/20261013000001_secret_broker/migration.sql +17 -0
- package/prisma/migrations/20261013000002_deferred_review_request/migration.sql +4 -0
- package/prisma/migrations/20261014000000_repo_skills/migration.sql +15 -0
- package/prisma/migrations/20261015000001_coding_run_merge_order_index/migration.sql +2 -0
- package/prisma/migrations/20261015000002_no_change_rereview/migration.sql +2 -0
- package/prisma/schema.prisma +362 -91
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { mkdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join, resolve, sep } from "node:path";
|
|
4
4
|
import { withBaseCommit } from "../../coding/base-commit.js";
|
|
5
|
+
import { buildClaudeContext, MAX_RECORDED_SKIPS } from "../../coding/claude-context.js";
|
|
5
6
|
import { normalizeCollectExclusions } from "../../coding/collect-exclude.js";
|
|
6
7
|
import { codingProviderForModelProvider } from "../../coding/provider.js";
|
|
7
8
|
import { CodingProfileSchema } from "../../coding/profile.js";
|
|
@@ -19,7 +20,7 @@ import { logger } from "../../core/logger.js";
|
|
|
19
20
|
import { requiredLevel } from "../../core/repo-access.js";
|
|
20
21
|
import { ensurePrivateDirectory } from "../../core/private-directory.js";
|
|
21
22
|
import { codingRunObserver } from "../../coding/observability.js";
|
|
22
|
-
import { CODING_PROTOCOL_VERSION, CodingRunResultSchema, CodingTaskInputSchema, parseCodingAgentOutputJson, redactAndTruncate, } from "../../coding/protocol.js";
|
|
23
|
+
import { CODING_PROTOCOL_VERSION, CodingRunResultSchema, CodingTaskInputSchema, MAX_CODING_INPUT_BYTES, parseCodingAgentOutputJson, redactAndTruncate, } from "../../coding/protocol.js";
|
|
23
24
|
import { parseStoredEntry } from "../llm/catalog-types.js";
|
|
24
25
|
import { resolveCodingEntry } from "../../core/run-pricing.js";
|
|
25
26
|
import { CODING_PROXY_ALIAS, CODING_PROXY_PORT, isImmutableDockerImage } from "../jobs/docker-isolation.js";
|
|
@@ -28,6 +29,8 @@ import { ISSUE_KEY, ISSUE_TRACKER_NAMES } from "../issue-tracker/types.js";
|
|
|
28
29
|
import { HEARTBEAT_TIMEOUT_MS } from "../../core/timing.js";
|
|
29
30
|
import { collectRelatedPullRequests } from "../../core/related-pull-requests.js";
|
|
30
31
|
const containerLog = logger.child({ module: "container-executor" });
|
|
32
|
+
/** A logged context skip path is cut to this many characters: a refused import is reported as written. */
|
|
33
|
+
const MAX_LOGGED_SKIP_PATH_CHARS = 256;
|
|
31
34
|
/** The input path in a spec built only to ask the launcher about capacity; never written or read. */
|
|
32
35
|
const CAPACITY_PROBE_INPUT = "/dev/null";
|
|
33
36
|
const TERMINAL_STATUSES = new Set(["succeeded", "failed", "refused", "lost", "budget_exhausted", "cancelled"]);
|
|
@@ -109,6 +112,7 @@ export class PrismaContainerExecutionStore {
|
|
|
109
112
|
rootCodingRunId: row.codingRun.rootCodingRunId,
|
|
110
113
|
issueProvider: row.codingRun.issueProvider,
|
|
111
114
|
issueKey: row.codingRun.issueKey,
|
|
115
|
+
mergeOrder: row.codingRun.mergeOrder,
|
|
112
116
|
budgetUsd: Number(row.codingRun.budgetReservedUsd),
|
|
113
117
|
agentBudgetUsd: Number(row.agent.budgetUsd),
|
|
114
118
|
budgetGroupName: row.agent.budgetGroup?.name ?? null,
|
|
@@ -126,6 +130,8 @@ export class PrismaContainerExecutionStore {
|
|
|
126
130
|
workspaceDiskMb: row.codingRun.workspaceDiskMb,
|
|
127
131
|
debugTrace: row.codingRun.debugTrace,
|
|
128
132
|
maxTurns: row.codingRun.maxTurns,
|
|
133
|
+
repoSkills: row.codingRun.repoSkills,
|
|
134
|
+
claudeBareMode: row.codingRun.claudeBareMode,
|
|
129
135
|
services: row.codingRun.services,
|
|
130
136
|
failureCategory: row.codingRun.failureCategory,
|
|
131
137
|
profileRepository: row.agent.codingProfile?.repository ?? null,
|
|
@@ -141,6 +147,7 @@ export class PrismaContainerExecutionStore {
|
|
|
141
147
|
repository: pr.repository,
|
|
142
148
|
number: pr.number,
|
|
143
149
|
...(pr.state ? { state: pr.state } : {}),
|
|
150
|
+
...(pr.mergeOrder !== undefined ? { mergeOrder: pr.mergeOrder } : {}),
|
|
144
151
|
}));
|
|
145
152
|
}
|
|
146
153
|
async claimProvisioning(runId, claimId) {
|
|
@@ -545,7 +552,7 @@ export class ContainerExecutor {
|
|
|
545
552
|
spendEnabled = true;
|
|
546
553
|
sessionId = session.id;
|
|
547
554
|
this.options.capabilities.set(runId, session.capability);
|
|
548
|
-
const inputArtifact = await this.writeInput(run, deadlineAt, workspace
|
|
555
|
+
const inputArtifact = await this.writeInput(run, deadlineAt, workspace);
|
|
549
556
|
const beforeLaunch = await this.requireCurrent(runId);
|
|
550
557
|
if (TERMINAL_STATUSES.has(beforeLaunch.status))
|
|
551
558
|
throw new Error("coding_run_no_longer_active");
|
|
@@ -715,7 +722,14 @@ export class ContainerExecutor {
|
|
|
715
722
|
const { issue } = this.issueFor(run);
|
|
716
723
|
return {
|
|
717
724
|
related: {
|
|
718
|
-
entries: [
|
|
725
|
+
entries: [
|
|
726
|
+
...earlier,
|
|
727
|
+
{
|
|
728
|
+
repository: run.repository,
|
|
729
|
+
self: true,
|
|
730
|
+
...(run.mergeOrder != null ? { mergeOrder: run.mergeOrder } : {}),
|
|
731
|
+
},
|
|
732
|
+
],
|
|
719
733
|
...(issue ? { issue } : {}),
|
|
720
734
|
},
|
|
721
735
|
};
|
|
@@ -920,6 +934,19 @@ export class ContainerExecutor {
|
|
|
920
934
|
}
|
|
921
935
|
await rm(this.artifactPath(run.runId), { recursive: true, force: true }).catch(() => undefined);
|
|
922
936
|
this.emit({ stage: "cleanup", runId: run.runId, jobId: handle.id, cleanupSucceeded: true });
|
|
937
|
+
await this.notifyTerminal(run.runId);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
/** Runs the onCodingRunTerminal hook; nothing it does, throws, or rejects reaches the caller. */
|
|
941
|
+
async notifyTerminal(runId) {
|
|
942
|
+
const hook = this.options.onCodingRunTerminal;
|
|
943
|
+
if (!hook)
|
|
944
|
+
return;
|
|
945
|
+
try {
|
|
946
|
+
await hook(runId);
|
|
947
|
+
}
|
|
948
|
+
catch (err) {
|
|
949
|
+
containerLog.warn({ err, runId }, "coding run terminal hook failed");
|
|
923
950
|
}
|
|
924
951
|
}
|
|
925
952
|
/** The package report is informational: any failure loading it (a
|
|
@@ -965,8 +992,10 @@ export class ContainerExecutor {
|
|
|
965
992
|
}
|
|
966
993
|
resolveCodingWorkerImage(selector) {
|
|
967
994
|
if (selector.provider === "claude-code") {
|
|
995
|
+
// workerImageRef never replaces the Claude Code agent image (the container that holds the
|
|
996
|
+
// run's key); on a Claude Code agent it selects the tool runner (claudeToolImage).
|
|
968
997
|
this.claudeToolImage(selector);
|
|
969
|
-
const image =
|
|
998
|
+
const image = this.options.claudeWorkerImage;
|
|
970
999
|
if (!image)
|
|
971
1000
|
throw new Error("coding_provider_not_configured:claude-code");
|
|
972
1001
|
if (!isImmutableDockerImage(image))
|
|
@@ -976,7 +1005,7 @@ export class ContainerExecutor {
|
|
|
976
1005
|
if (selector.provider !== "codex") {
|
|
977
1006
|
throw new Error(`coding_provider_unsupported:${String(selector.provider)}`);
|
|
978
1007
|
}
|
|
979
|
-
if (selector.workerImageRef) {
|
|
1008
|
+
if (selector.workerImageRef !== null) {
|
|
980
1009
|
if (!isImmutableDockerImage(selector.workerImageRef))
|
|
981
1010
|
throw new Error("coding_worker_image_invalid");
|
|
982
1011
|
return selector.workerImageRef;
|
|
@@ -997,10 +1026,20 @@ export class ContainerExecutor {
|
|
|
997
1026
|
resolveCodingToolImage(selector) {
|
|
998
1027
|
return selector.provider === "claude-code" ? this.claudeToolImage(selector) : null;
|
|
999
1028
|
}
|
|
1000
|
-
/**
|
|
1029
|
+
/**
|
|
1030
|
+
* The tool runner is what runs a Claude agent's commands, so it is what the toolchain selects —
|
|
1031
|
+
* unless the agent has a BYO workerImageRef, which replaces the tool runner directly (never the
|
|
1032
|
+
* agent image; see resolveCodingWorkerImage).
|
|
1033
|
+
*/
|
|
1001
1034
|
claudeToolImage(selector) {
|
|
1002
1035
|
if (!this.options.claudeToolRunnerImage)
|
|
1003
1036
|
throw new Error("coding_provider_not_configured:claude-code");
|
|
1037
|
+
// An admin-set custom image (workerImageRef) is where this agent's commands run.
|
|
1038
|
+
if (selector.workerImageRef !== null) {
|
|
1039
|
+
if (!isImmutableDockerImage(selector.workerImageRef))
|
|
1040
|
+
throw new Error("coding_worker_image_invalid");
|
|
1041
|
+
return selector.workerImageRef;
|
|
1042
|
+
}
|
|
1004
1043
|
const image = selector.toolchain === "node" && selector.toolchainVersion === null
|
|
1005
1044
|
? this.options.claudeToolRunnerImage
|
|
1006
1045
|
: selector.toolchainVersion !== null
|
|
@@ -1038,6 +1077,10 @@ export class ContainerExecutor {
|
|
|
1038
1077
|
supportsCodingServices(provider) {
|
|
1039
1078
|
return this.options.jobs.supportsServicesFor?.(provider) === true;
|
|
1040
1079
|
}
|
|
1080
|
+
/** Delegates to the job launcher's own warm-up (Executor.warmUp); a no-op for a launcher without one. */
|
|
1081
|
+
async warmUp() {
|
|
1082
|
+
await this.options.jobs.warmUp?.();
|
|
1083
|
+
}
|
|
1041
1084
|
async requireCurrent(runId) {
|
|
1042
1085
|
const current = await this.options.store.load(runId);
|
|
1043
1086
|
if (!current)
|
|
@@ -1099,7 +1142,7 @@ export class ContainerExecutor {
|
|
|
1099
1142
|
return this.options.anthropicCredentialRef;
|
|
1100
1143
|
throw new Error("coding_provider_not_configured:claude-code");
|
|
1101
1144
|
}
|
|
1102
|
-
async writeInput(run, deadlineAt,
|
|
1145
|
+
async writeInput(run, deadlineAt, workspace) {
|
|
1103
1146
|
await ensurePrivateDirectory(this.artifactRoot);
|
|
1104
1147
|
const rootReal = await realpath(this.artifactRoot);
|
|
1105
1148
|
const directory = resolve(rootReal, run.runId);
|
|
@@ -1111,11 +1154,24 @@ export class ContainerExecutor {
|
|
|
1111
1154
|
const destination = join(directory, "input.json");
|
|
1112
1155
|
const temporary = join(directory, `.input-${randomUUID()}.tmp`);
|
|
1113
1156
|
const services = parseStoredServices(run.services);
|
|
1114
|
-
const task = withBaseCommit(run.task, baseCommit);
|
|
1115
|
-
if (task === run.task && /^[0-9a-f]{40}$/.test(baseCommit)) {
|
|
1157
|
+
const task = withBaseCommit(run.task, workspace.baseCommit);
|
|
1158
|
+
if (task === run.task && /^[0-9a-f]{40}$/.test(workspace.baseCommit)) {
|
|
1116
1159
|
containerLog.warn({ event: "coding.base_commit_omitted", runId: run.runId }, "the coding task left no room for the base commit line");
|
|
1117
1160
|
}
|
|
1118
|
-
|
|
1161
|
+
// A repository context read must never fail a run ("the run never fails because of context"):
|
|
1162
|
+
// any fs error (an inaccessible workspace, EACCES, ...) is logged once and swallowed, leaving
|
|
1163
|
+
// claudeContext null, exactly as if the run had nothing to load.
|
|
1164
|
+
let claudeContext = null;
|
|
1165
|
+
if (run.provider === "claude-code") {
|
|
1166
|
+
try {
|
|
1167
|
+
claudeContext = await buildClaudeContext(workspace.workspacePath, { skills: run.repoSkills !== false });
|
|
1168
|
+
}
|
|
1169
|
+
catch (error) {
|
|
1170
|
+
const code = error?.code;
|
|
1171
|
+
containerLog.warn({ event: "coding.claude_context_unavailable", runId: run.runId, ...(code ? { code } : {}) }, "repository context could not be read; continuing without it");
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
const build = (contextFiles) => CodingTaskInputSchema.parse({
|
|
1119
1175
|
schemaVersion: CODING_PROTOCOL_VERSION,
|
|
1120
1176
|
runId: run.runId,
|
|
1121
1177
|
repository: run.repository,
|
|
@@ -1130,11 +1186,45 @@ export class ContainerExecutor {
|
|
|
1130
1186
|
...(run.debugTrace ? { debugTrace: true } : {}),
|
|
1131
1187
|
// Only when the agent sets one: every other run's input stays exactly what older workers expect.
|
|
1132
1188
|
...(run.maxTurns ? { maxTurns: run.maxTurns } : {}),
|
|
1189
|
+
// Only when off: every other run's input stays exactly what older workers expect.
|
|
1190
|
+
...(run.repoSkills === false ? { repoSkills: false } : {}),
|
|
1191
|
+
// Only when off: every other run's input stays exactly what older workers expect.
|
|
1192
|
+
...(run.claudeBareMode === false ? { claudeBareMode: false } : {}),
|
|
1133
1193
|
// Only when there are some: every other run's input stays exactly what older workers expect.
|
|
1134
1194
|
...(services.length > 0 ? { services: workerServices(services) } : {}),
|
|
1195
|
+
// Only when there is something to load: every other run's input stays exactly what older workers expect.
|
|
1196
|
+
...(contextFiles.length > 0 ? { claudeContext: { files: contextFiles } } : {}),
|
|
1135
1197
|
});
|
|
1198
|
+
// The content limits bound bytes, but JSON escaping can grow them up to sixfold (a control
|
|
1199
|
+
// character becomes \u00XX), so a hostile repository could still push input.json past
|
|
1200
|
+
// MAX_CODING_INPUT_BYTES and fail the launch. Drop context files from the tail (skills, then the
|
|
1201
|
+
// deepest imports; the root instructions go last) until the serialized input fits.
|
|
1202
|
+
const contextFiles = [...(claudeContext?.files ?? [])];
|
|
1203
|
+
const skipped = [...(claudeContext?.skipped ?? [])];
|
|
1204
|
+
let skippedOverflow = claudeContext?.skippedOverflow ?? 0;
|
|
1205
|
+
let serialized = JSON.stringify(build(contextFiles));
|
|
1206
|
+
while (contextFiles.length > 0 && Buffer.byteLength(serialized) > MAX_CODING_INPUT_BYTES) {
|
|
1207
|
+
const dropped = contextFiles.pop();
|
|
1208
|
+
if (skipped.length < MAX_RECORDED_SKIPS)
|
|
1209
|
+
skipped.push({ path: dropped.path, reason: "limit" });
|
|
1210
|
+
else
|
|
1211
|
+
skippedOverflow++;
|
|
1212
|
+
serialized = JSON.stringify(build(contextFiles));
|
|
1213
|
+
}
|
|
1214
|
+
for (const skip of skipped) {
|
|
1215
|
+
containerLog.warn({
|
|
1216
|
+
event: "coding.claude_context_skipped",
|
|
1217
|
+
runId: run.runId,
|
|
1218
|
+
// A refused import is logged as written, which a repository can make arbitrarily long.
|
|
1219
|
+
path: skip.path.slice(0, MAX_LOGGED_SKIP_PATH_CHARS),
|
|
1220
|
+
reason: skip.reason,
|
|
1221
|
+
}, "a repository context file was not loaded");
|
|
1222
|
+
}
|
|
1223
|
+
if (skippedOverflow > 0) {
|
|
1224
|
+
containerLog.warn({ event: "coding.claude_context_skipped", runId: run.runId, overflow: skippedOverflow }, "further repository context files were not loaded");
|
|
1225
|
+
}
|
|
1136
1226
|
// The run directory is 0700; world-readable mode only crosses Docker's UID boundary.
|
|
1137
|
-
await writeFile(temporary,
|
|
1227
|
+
await writeFile(temporary, serialized, { flag: "wx", mode: 0o444 });
|
|
1138
1228
|
await rename(temporary, destination);
|
|
1139
1229
|
return destination;
|
|
1140
1230
|
}
|
|
@@ -11,14 +11,56 @@
|
|
|
11
11
|
* The heartbeat is kept while the workflow runs here so the reconciler's
|
|
12
12
|
* stale-heartbeat detection is unchanged; the reconciler consults
|
|
13
13
|
* `recover()` (Task 6) before declaring a DBOS run lost.
|
|
14
|
+
*
|
|
15
|
+
* A stalled-but-alive owner can overlap an adopted attempt of the same run:
|
|
16
|
+
* whichever records a step second loses ownership, stops at that step
|
|
17
|
+
* without writing the run, and hands the SDK's conflict back so DBOS parks
|
|
18
|
+
* it as a duplicate execution (see dbosStep and runInsideWorkflow).
|
|
14
19
|
*/
|
|
20
|
+
import pg from "pg";
|
|
15
21
|
import type { PrismaClient } from "#prisma";
|
|
16
22
|
import type { DbosConfig } from "../../config/providers.js";
|
|
23
|
+
import { type StepRunner } from "../engine/types.js";
|
|
17
24
|
import { type NativeRunProviders, type RunnerDb } from "../../core/runner.js";
|
|
25
|
+
import { type DatabaseAuth, type PgAuthOptions } from "../../core/pg-auth.js";
|
|
18
26
|
import type { Executor, ExecutionRecoveryResult, PersistedExecutionHandle } from "./types.js";
|
|
19
27
|
export declare const DBOS_BACKEND = "dbos";
|
|
20
28
|
type Providers = NativeRunProviders;
|
|
21
29
|
type Db = RunnerDb & Pick<PrismaClient, "run">;
|
|
30
|
+
/**
|
|
31
|
+
* DBOS.setConfig input. Under password auth this is exactly what launch() always passed, and DBOS
|
|
32
|
+
* opens its own pool from the URL. Under rds-iam DBOS gets a pool whose password is the IAM token;
|
|
33
|
+
* DBOS then runs its migrations, queries and LISTEN through that pool and never opens a URL
|
|
34
|
+
* connection (system_database.js: a custom pool skips database creation and is what every
|
|
35
|
+
* connection borrows from). The pool copies what DBOS would have set on its own: size, connect
|
|
36
|
+
* timeout and application_name. DBOS never ends a pool it was given — see endSystemPool().
|
|
37
|
+
*/
|
|
38
|
+
export declare function dbosLaunchConfig(config: DbosConfig, executorId: string, auth: DatabaseAuth, pgAuth?: PgAuthOptions): {
|
|
39
|
+
name: string;
|
|
40
|
+
systemDatabaseUrl: string;
|
|
41
|
+
systemDatabaseSchemaName: string;
|
|
42
|
+
executorID: string;
|
|
43
|
+
runAdminServer: false;
|
|
44
|
+
logLevel: "warn";
|
|
45
|
+
systemDatabasePool?: pg.Pool;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Bind the engine's step boundary to a DBOS checkpointed step. Never
|
|
49
|
+
* retried: a retry would re-spend.
|
|
50
|
+
*
|
|
51
|
+
* DBOS signals a cancelled workflow by throwing `DBOSWorkflowCancelledError`
|
|
52
|
+
* out of the next step boundary. Translated here into a `RunCancelledError`
|
|
53
|
+
* so executeRun's backstop records `cancelled` plus the operator's reason,
|
|
54
|
+
* rather than `failed` plus an SDK-internal message.
|
|
55
|
+
*
|
|
56
|
+
* DBOS signals that another execution of this workflow recorded the step
|
|
57
|
+
* first by throwing `DBOSWorkflowConflictError` after this attempt's step
|
|
58
|
+
* body already ran. Translated into a `RunOwnershipLostError` (carrying the
|
|
59
|
+
* discarded step's usage, if any, for the log) so executeRun's backstop stops
|
|
60
|
+
* this attempt without writing the run; runInsideWorkflow turns it back into
|
|
61
|
+
* the SDK error.
|
|
62
|
+
*/
|
|
63
|
+
export declare const dbosStep: StepRunner;
|
|
22
64
|
export declare class DbosExecutor implements Executor {
|
|
23
65
|
private readonly providers;
|
|
24
66
|
private readonly config;
|
|
@@ -11,16 +11,67 @@
|
|
|
11
11
|
* The heartbeat is kept while the workflow runs here so the reconciler's
|
|
12
12
|
* stale-heartbeat detection is unchanged; the reconciler consults
|
|
13
13
|
* `recover()` (Task 6) before declaring a DBOS run lost.
|
|
14
|
+
*
|
|
15
|
+
* A stalled-but-alive owner can overlap an adopted attempt of the same run:
|
|
16
|
+
* whichever records a step second loses ownership, stops at that step
|
|
17
|
+
* without writing the run, and hands the SDK's conflict back so DBOS parks
|
|
18
|
+
* it as a duplicate execution (see dbosStep and runInsideWorkflow).
|
|
14
19
|
*/
|
|
15
20
|
import { DBOS, Error as DbosErrors } from "@dbos-inc/dbos-sdk";
|
|
21
|
+
import pg from "pg";
|
|
22
|
+
import { RunOwnershipLostError } from "../engine/types.js";
|
|
16
23
|
import { executeRun, RunCancelledError } from "../../core/runner.js";
|
|
17
24
|
import { markRunFailedFromExecutorError } from "../../core/dispatch.js";
|
|
18
25
|
import { prisma as defaultDb } from "../../core/db.js";
|
|
19
26
|
import { HEARTBEAT_INTERVAL_MS } from "../../core/timing.js";
|
|
20
27
|
import { logger } from "../../core/logger.js";
|
|
28
|
+
import { databaseAuthFromEnv, pgConnectionConfig } from "../../core/pg-auth.js";
|
|
21
29
|
import { ADOPTION_EXHAUSTED_REASON, decideRecovery, shouldGiveUpAdoption, versionMismatchReason, } from "./dbos-status.js";
|
|
22
30
|
export const DBOS_BACKEND = "dbos";
|
|
23
31
|
const dbosLog = logger.child({ module: "dbos-executor" });
|
|
32
|
+
/** DBOS's own system pool size (DEFAULT_POOL_SIZE in the SDK's system_database.js); kept for ours. */
|
|
33
|
+
const DBOS_SYSTEM_POOL_SIZE = 10;
|
|
34
|
+
/** DBOS's own connect timeout when the URL sets no connect_timeout (getClientConfig in the SDK's utils.js). */
|
|
35
|
+
const DBOS_CONNECT_TIMEOUT_MS = 10_000;
|
|
36
|
+
/**
|
|
37
|
+
* DBOS.setConfig input. Under password auth this is exactly what launch() always passed, and DBOS
|
|
38
|
+
* opens its own pool from the URL. Under rds-iam DBOS gets a pool whose password is the IAM token;
|
|
39
|
+
* DBOS then runs its migrations, queries and LISTEN through that pool and never opens a URL
|
|
40
|
+
* connection (system_database.js: a custom pool skips database creation and is what every
|
|
41
|
+
* connection borrows from). The pool copies what DBOS would have set on its own: size, connect
|
|
42
|
+
* timeout and application_name. DBOS never ends a pool it was given — see endSystemPool().
|
|
43
|
+
*/
|
|
44
|
+
export function dbosLaunchConfig(config, executorId, auth, pgAuth) {
|
|
45
|
+
const url = config.systemDatabaseUrl;
|
|
46
|
+
const launch = {
|
|
47
|
+
name: "wardby",
|
|
48
|
+
systemDatabaseUrl: url,
|
|
49
|
+
systemDatabaseSchemaName: config.schemaName,
|
|
50
|
+
executorID: executorId,
|
|
51
|
+
runAdminServer: false,
|
|
52
|
+
logLevel: "warn",
|
|
53
|
+
};
|
|
54
|
+
if (auth !== "rds-iam")
|
|
55
|
+
return launch;
|
|
56
|
+
const connection = pgConnectionConfig(url, auth, pgAuth); // validates the URL first
|
|
57
|
+
const connectTimeout = Number.parseInt(new URL(url).searchParams.get("connect_timeout") ?? "", 10);
|
|
58
|
+
const pool = new pg.Pool({
|
|
59
|
+
application_name: `dbos_transact_${executorId}`,
|
|
60
|
+
...connection,
|
|
61
|
+
max: DBOS_SYSTEM_POOL_SIZE,
|
|
62
|
+
connectionTimeoutMillis: connectTimeout ? connectTimeout * 1000 : DBOS_CONNECT_TIMEOUT_MS,
|
|
63
|
+
});
|
|
64
|
+
// DBOS attaches no listener to a caller's pool; an idle connection's error would otherwise crash the process.
|
|
65
|
+
pool.on("error", (err) => dbosLog.warn({ err }, "DBOS system database pool error"));
|
|
66
|
+
return { ...launch, systemDatabasePool: pool };
|
|
67
|
+
}
|
|
68
|
+
/** The pool launch() handed DBOS, if any. Module-level like DBOS itself: whoever shuts DBOS down ends it. */
|
|
69
|
+
let systemPool;
|
|
70
|
+
async function endSystemPool() {
|
|
71
|
+
const pool = systemPool;
|
|
72
|
+
systemPool = undefined;
|
|
73
|
+
await pool?.end().catch((err) => dbosLog.warn({ err }, "failed to close the DBOS system database pool"));
|
|
74
|
+
}
|
|
24
75
|
/** The executor whose deps the registered workflow uses. Set by launch(). */
|
|
25
76
|
let activeExecutor;
|
|
26
77
|
/** Setter (rather than `activeExecutor = this` inline) to avoid aliasing `this` to a variable. */
|
|
@@ -44,19 +95,48 @@ const cancellationReasons = new Map();
|
|
|
44
95
|
* out of the next step boundary. Translated here into a `RunCancelledError`
|
|
45
96
|
* so executeRun's backstop records `cancelled` plus the operator's reason,
|
|
46
97
|
* rather than `failed` plus an SDK-internal message.
|
|
98
|
+
*
|
|
99
|
+
* DBOS signals that another execution of this workflow recorded the step
|
|
100
|
+
* first by throwing `DBOSWorkflowConflictError` after this attempt's step
|
|
101
|
+
* body already ran. Translated into a `RunOwnershipLostError` (carrying the
|
|
102
|
+
* discarded step's usage, if any, for the log) so executeRun's backstop stops
|
|
103
|
+
* this attempt without writing the run; runInsideWorkflow turns it back into
|
|
104
|
+
* the SDK error.
|
|
47
105
|
*/
|
|
48
|
-
const dbosStep = async (name, fn) => {
|
|
106
|
+
export const dbosStep = async (name, fn) => {
|
|
107
|
+
let ran;
|
|
49
108
|
try {
|
|
50
|
-
return await DBOS.runStep(
|
|
109
|
+
return await DBOS.runStep(async () => {
|
|
110
|
+
const value = await fn();
|
|
111
|
+
ran = { value };
|
|
112
|
+
return value;
|
|
113
|
+
}, { name, retriesAllowed: false });
|
|
51
114
|
}
|
|
52
115
|
catch (err) {
|
|
53
116
|
if (err instanceof DbosErrors.DBOSWorkflowCancelledError) {
|
|
54
117
|
const reason = cancellationReasons.get(err.workflowID);
|
|
55
118
|
throw new RunCancelledError(`Run cancelled: ${reason ?? "no reason given"}`);
|
|
56
119
|
}
|
|
120
|
+
if (err instanceof DbosErrors.DBOSWorkflowConflictError) {
|
|
121
|
+
const discardedUsage = usageOf(ran?.value);
|
|
122
|
+
throw new RunOwnershipLostError(name, { cause: err, ...(discardedUsage ? { discardedUsage } : {}) });
|
|
123
|
+
}
|
|
57
124
|
throw err;
|
|
58
125
|
}
|
|
59
126
|
};
|
|
127
|
+
/**
|
|
128
|
+
* The numeric `usage` fields of a step result, if it has any (an LLM turn's
|
|
129
|
+
* token counts and cost). Tool steps return a string and yield nothing.
|
|
130
|
+
*/
|
|
131
|
+
function usageOf(value) {
|
|
132
|
+
if (typeof value !== "object" || value === null || !("usage" in value))
|
|
133
|
+
return undefined;
|
|
134
|
+
const usage = value.usage;
|
|
135
|
+
if (typeof usage !== "object" || usage === null)
|
|
136
|
+
return undefined;
|
|
137
|
+
const numeric = Object.entries(usage).filter((entry) => typeof entry[1] === "number");
|
|
138
|
+
return numeric.length > 0 ? Object.fromEntries(numeric) : undefined;
|
|
139
|
+
}
|
|
60
140
|
/**
|
|
61
141
|
* Registered once at module load — DBOS requires registration before
|
|
62
142
|
* launch(). The body must be deterministic between steps; everything
|
|
@@ -112,15 +192,24 @@ export class DbosExecutor {
|
|
|
112
192
|
}
|
|
113
193
|
setActiveExecutor(this);
|
|
114
194
|
if (!DBOS.isInitialized()) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
195
|
+
const launchConfig = dbosLaunchConfig(this.config, this.id, databaseAuthFromEnv());
|
|
196
|
+
systemPool = launchConfig.systemDatabasePool;
|
|
197
|
+
try {
|
|
198
|
+
DBOS.setConfig(launchConfig);
|
|
199
|
+
await DBOS.launch();
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
// DBOS.launch can throw after it initialized (registering the application version, starting
|
|
203
|
+
// event receivers). Shut that half-launched DBOS down so the next launch() starts clean, then
|
|
204
|
+
// end the pool either way: the next launch() makes a new one.
|
|
205
|
+
if (activeExecutor === this)
|
|
206
|
+
setActiveExecutor(undefined);
|
|
207
|
+
if (DBOS.isInitialized()) {
|
|
208
|
+
await DBOS.shutdown().catch((shutdownErr) => dbosLog.warn({ err: shutdownErr }, "failed to shut down DBOS after a failed launch"));
|
|
209
|
+
}
|
|
210
|
+
await endSystemPool();
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
124
213
|
}
|
|
125
214
|
this.launched = true;
|
|
126
215
|
dbosLog.info({ executorId: DBOS.executorID }, "DBOS executor launched");
|
|
@@ -131,7 +220,12 @@ export class DbosExecutor {
|
|
|
131
220
|
this.launched = false;
|
|
132
221
|
if (activeExecutor === this)
|
|
133
222
|
setActiveExecutor(undefined);
|
|
134
|
-
|
|
223
|
+
try {
|
|
224
|
+
await DBOS.shutdown({ workflowCompletionTimeoutMS: 5_000 });
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
await endSystemPool();
|
|
228
|
+
}
|
|
135
229
|
}
|
|
136
230
|
/** Where a run this executor marks failed files its self-defect (no-op without a configured issue tracker). */
|
|
137
231
|
get selfDefects() {
|
|
@@ -263,6 +357,18 @@ export class DbosExecutor {
|
|
|
263
357
|
try {
|
|
264
358
|
await executeRun(runId, this.providers, this.db, undefined, dbosStep);
|
|
265
359
|
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
// An ownership loss leaves the workflow body as the SDK's own conflict error, so DBOS takes
|
|
362
|
+
// its duplicate-execution path: it records nothing and parks this attempt until the winner
|
|
363
|
+
// records the outcome. Any other error escaping here would be recorded as the workflow's
|
|
364
|
+
// outcome while the winner still runs. The runner already logged the loss.
|
|
365
|
+
if (err instanceof RunOwnershipLostError) {
|
|
366
|
+
throw err.cause instanceof DbosErrors.DBOSWorkflowConflictError
|
|
367
|
+
? err.cause
|
|
368
|
+
: new DbosErrors.DBOSWorkflowConflictError(runId);
|
|
369
|
+
}
|
|
370
|
+
throw err;
|
|
371
|
+
}
|
|
266
372
|
finally {
|
|
267
373
|
clearInterval(timer);
|
|
268
374
|
cancellationReasons.delete(runId);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An Executor for a process that must never start or stop runs itself: the
|
|
3
|
+
* native sandbox gateway (docs/native-sandbox.md). Sandbox workers can reach
|
|
4
|
+
* the gateway, so it holds no Docker socket, cluster credential, or in-process
|
|
5
|
+
* engine; a child it dispatches is only recorded (Run.startDeferredAt) and a
|
|
6
|
+
* stop it needs only requested (Run.stopRequestedAt). The scheduler leader
|
|
7
|
+
* carries both out through the server's own executor (`drainDeferredRuns`).
|
|
8
|
+
*
|
|
9
|
+
* Coding dispatch still needs the worker image and the repository's service
|
|
10
|
+
* declaration and knowledge files, resolved at dispatch: those come from a
|
|
11
|
+
* resolve-only view of the configured coding executor, whose start/stop are
|
|
12
|
+
* never called here.
|
|
13
|
+
*/
|
|
14
|
+
import type { PrismaClient } from "#prisma";
|
|
15
|
+
import type { CodingImageSelector, Executor } from "./types.js";
|
|
16
|
+
import type { CodingProvider } from "../../coding/provider.js";
|
|
17
|
+
export type DeferredExecutorDb = Pick<PrismaClient, "run">;
|
|
18
|
+
export declare class DeferredExecutor implements Executor {
|
|
19
|
+
private readonly db;
|
|
20
|
+
/** Coding resolution only (image, service declaration, repository files); never started or stopped. */
|
|
21
|
+
private readonly resolver?;
|
|
22
|
+
constructor(db: DeferredExecutorDb,
|
|
23
|
+
/** Coding resolution only (image, service declaration, repository files); never started or stopped. */
|
|
24
|
+
resolver?: Executor | undefined);
|
|
25
|
+
start(runId: string): Promise<void>;
|
|
26
|
+
stop(runId: string, reason?: string): Promise<void>;
|
|
27
|
+
resolveCodingWorkerImage(selector: CodingImageSelector): string;
|
|
28
|
+
resolveCodingToolImage(selector: CodingImageSelector): string | null;
|
|
29
|
+
readCodingServiceDeclaration(input: {
|
|
30
|
+
repository: string;
|
|
31
|
+
baseRef: string;
|
|
32
|
+
}): Promise<string | null>;
|
|
33
|
+
readCodingRepositoryFile(input: {
|
|
34
|
+
repository: string;
|
|
35
|
+
baseRef: string;
|
|
36
|
+
path: string;
|
|
37
|
+
maxBytes: number;
|
|
38
|
+
}): Promise<string | null>;
|
|
39
|
+
supportsCodingServices(provider: CodingProvider): boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface DrainDeferredRunsOptions {
|
|
42
|
+
db: Pick<PrismaClient, "run">;
|
|
43
|
+
executor: Executor;
|
|
44
|
+
/** Where a start failure is recorded; defaults to marking the run failed with the error. */
|
|
45
|
+
onStartFailed?: (runId: string, err: unknown) => Promise<void>;
|
|
46
|
+
limit?: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Starts runs a deferred executor dispatched, and stops runs it asked to stop, through the
|
|
50
|
+
* server's executor. Called on the scheduler leader's tick. Each row is claimed by clearing its
|
|
51
|
+
* marker in a conditional update first, so two drains never start or stop the same run twice.
|
|
52
|
+
*/
|
|
53
|
+
export declare function drainDeferredRuns(options: DrainDeferredRunsOptions): Promise<{
|
|
54
|
+
started: number;
|
|
55
|
+
stopped: number;
|
|
56
|
+
}>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An Executor for a process that must never start or stop runs itself: the
|
|
3
|
+
* native sandbox gateway (docs/native-sandbox.md). Sandbox workers can reach
|
|
4
|
+
* the gateway, so it holds no Docker socket, cluster credential, or in-process
|
|
5
|
+
* engine; a child it dispatches is only recorded (Run.startDeferredAt) and a
|
|
6
|
+
* stop it needs only requested (Run.stopRequestedAt). The scheduler leader
|
|
7
|
+
* carries both out through the server's own executor (`drainDeferredRuns`).
|
|
8
|
+
*
|
|
9
|
+
* Coding dispatch still needs the worker image and the repository's service
|
|
10
|
+
* declaration and knowledge files, resolved at dispatch: those come from a
|
|
11
|
+
* resolve-only view of the configured coding executor, whose start/stop are
|
|
12
|
+
* never called here.
|
|
13
|
+
*/
|
|
14
|
+
import { logger } from "../../core/logger.js";
|
|
15
|
+
const deferredLog = logger.child({ module: "deferred-executor" });
|
|
16
|
+
export class DeferredExecutor {
|
|
17
|
+
db;
|
|
18
|
+
resolver;
|
|
19
|
+
constructor(db,
|
|
20
|
+
/** Coding resolution only (image, service declaration, repository files); never started or stopped. */
|
|
21
|
+
resolver) {
|
|
22
|
+
this.db = db;
|
|
23
|
+
this.resolver = resolver;
|
|
24
|
+
}
|
|
25
|
+
async start(runId) {
|
|
26
|
+
await this.db.run.updateMany({ where: { id: runId, status: "pending" }, data: { startDeferredAt: new Date() } });
|
|
27
|
+
}
|
|
28
|
+
async stop(runId, reason) {
|
|
29
|
+
// Never started: nothing runs it, so it can simply end here.
|
|
30
|
+
const cancelled = await this.db.run.updateMany({
|
|
31
|
+
where: { id: runId, status: "pending", startDeferredAt: { not: null } },
|
|
32
|
+
data: {
|
|
33
|
+
status: "cancelled",
|
|
34
|
+
error: reason ?? "Cancelled before it started.",
|
|
35
|
+
finishedAt: new Date(),
|
|
36
|
+
startDeferredAt: null,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
if (cancelled.count > 0)
|
|
40
|
+
return;
|
|
41
|
+
await this.db.run.updateMany({
|
|
42
|
+
where: { id: runId, status: { in: ["pending", "running"] } },
|
|
43
|
+
data: { stopRequestedAt: new Date(), stopRequestReason: reason ?? null },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
resolveCodingWorkerImage(selector) {
|
|
47
|
+
if (!this.resolver?.resolveCodingWorkerImage)
|
|
48
|
+
throw new Error("coding_execution_not_configured");
|
|
49
|
+
return this.resolver.resolveCodingWorkerImage(selector);
|
|
50
|
+
}
|
|
51
|
+
resolveCodingToolImage(selector) {
|
|
52
|
+
if (!this.resolver?.resolveCodingToolImage)
|
|
53
|
+
throw new Error("coding_execution_not_configured");
|
|
54
|
+
return this.resolver.resolveCodingToolImage(selector);
|
|
55
|
+
}
|
|
56
|
+
async readCodingServiceDeclaration(input) {
|
|
57
|
+
return this.resolver?.readCodingServiceDeclaration?.(input) ?? null;
|
|
58
|
+
}
|
|
59
|
+
async readCodingRepositoryFile(input) {
|
|
60
|
+
return this.resolver?.readCodingRepositoryFile?.(input) ?? null;
|
|
61
|
+
}
|
|
62
|
+
supportsCodingServices(provider) {
|
|
63
|
+
return this.resolver?.supportsCodingServices?.(provider) === true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Starts runs a deferred executor dispatched, and stops runs it asked to stop, through the
|
|
68
|
+
* server's executor. Called on the scheduler leader's tick. Each row is claimed by clearing its
|
|
69
|
+
* marker in a conditional update first, so two drains never start or stop the same run twice.
|
|
70
|
+
*/
|
|
71
|
+
export async function drainDeferredRuns(options) {
|
|
72
|
+
const { db, executor } = options;
|
|
73
|
+
const limit = options.limit ?? 50;
|
|
74
|
+
let started = 0;
|
|
75
|
+
let stopped = 0;
|
|
76
|
+
const toStart = await db.run.findMany({
|
|
77
|
+
where: { startDeferredAt: { not: null }, status: "pending" },
|
|
78
|
+
orderBy: { startDeferredAt: "asc" },
|
|
79
|
+
take: limit,
|
|
80
|
+
select: { id: true },
|
|
81
|
+
});
|
|
82
|
+
for (const { id } of toStart) {
|
|
83
|
+
const claimed = await db.run.updateMany({
|
|
84
|
+
where: { id, startDeferredAt: { not: null }, status: "pending" },
|
|
85
|
+
data: { startDeferredAt: null },
|
|
86
|
+
});
|
|
87
|
+
if (claimed.count === 0)
|
|
88
|
+
continue;
|
|
89
|
+
started += 1;
|
|
90
|
+
// Not awaited: an in-process executor's start runs the whole run.
|
|
91
|
+
void executor.start(id).catch(async (err) => {
|
|
92
|
+
deferredLog.error({ err, runId: id }, "deferred run failed to start");
|
|
93
|
+
await (options.onStartFailed?.(id, err) ??
|
|
94
|
+
db.run.updateMany({
|
|
95
|
+
where: { id, status: { in: ["pending", "running"] } },
|
|
96
|
+
data: { status: "failed", error: err instanceof Error ? err.message : String(err), finishedAt: new Date() },
|
|
97
|
+
}));
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const toStop = await db.run.findMany({
|
|
101
|
+
where: { stopRequestedAt: { not: null } },
|
|
102
|
+
orderBy: { stopRequestedAt: "asc" },
|
|
103
|
+
take: limit,
|
|
104
|
+
select: { id: true, stopRequestReason: true, status: true },
|
|
105
|
+
});
|
|
106
|
+
for (const row of toStop) {
|
|
107
|
+
const claimed = await db.run.updateMany({
|
|
108
|
+
where: { id: row.id, stopRequestedAt: { not: null } },
|
|
109
|
+
data: { stopRequestedAt: null, stopRequestReason: null },
|
|
110
|
+
});
|
|
111
|
+
if (claimed.count === 0 || !["pending", "running"].includes(row.status))
|
|
112
|
+
continue;
|
|
113
|
+
stopped += 1;
|
|
114
|
+
await executor.stop(row.id, row.stopRequestReason ?? undefined).catch((err) => {
|
|
115
|
+
deferredLog.warn({ err, runId: row.id }, "deferred stop request failed");
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return { started, stopped };
|
|
119
|
+
}
|