@wardby/cli 0.5.3 → 0.7.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/deploy/local/docker-compose.quickstart-coding.yml +13 -0
- 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 +27 -3
- package/dist/claude-coding-worker/driver.js +161 -20
- package/dist/claude-coding-worker/main.js +34 -6
- package/dist/claude-coding-worker/sdk.js +25 -5
- package/dist/claude-coding-worker/tool-socket.d.ts +27 -0
- package/dist/claude-coding-worker/tool-socket.js +111 -0
- package/dist/claude-coding-worker/transcript.d.ts +18 -0
- package/dist/claude-coding-worker/transcript.js +77 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +6 -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 +161 -11
- package/dist/coding/claude-context.d.ts +45 -0
- package/dist/coding/claude-context.js +434 -0
- package/dist/coding/continuation-wording.d.ts +35 -0
- package/dist/coding/continuation-wording.js +73 -0
- package/dist/coding/linked-issue-note.d.ts +23 -0
- package/dist/coding/linked-issue-note.js +47 -0
- package/dist/coding/merge-wording.d.ts +28 -0
- package/dist/coding/merge-wording.js +78 -0
- package/dist/coding/pr-description-note.d.ts +9 -0
- package/dist/coding/pr-description-note.js +32 -0
- package/dist/coding/profile.d.ts +50 -118
- package/dist/coding/profile.js +51 -4
- package/dist/coding/protected-path-wording.d.ts +8 -0
- package/dist/coding/protected-path-wording.js +1 -1
- package/dist/coding/protocol.d.ts +292 -270
- package/dist/coding/protocol.js +322 -15
- package/dist/coding/redact.d.ts +3 -0
- package/dist/coding/redact.js +12 -0
- package/dist/coding/registry/allowlist.d.ts +12 -0
- package/dist/coding/registry/allowlist.js +14 -1
- package/dist/coding/registry/audit-mode.d.ts +153 -0
- package/dist/coding/registry/audit-mode.js +304 -0
- package/dist/coding/registry/lockfile-shared.d.ts +36 -0
- package/dist/coding/registry/lockfile-shared.js +128 -0
- package/dist/coding/registry/lockfiles.js +1 -1
- package/dist/coding/registry/npm-lockfile-formats.d.ts +11 -0
- package/dist/coding/registry/npm-lockfile-formats.js +41 -0
- package/dist/coding/registry/npm-lockfile.d.ts +8 -4
- package/dist/coding/registry/npm-lockfile.js +4 -6
- package/dist/coding/registry/npm.js +78 -8
- package/dist/coding/registry/pnpm-lockfile.d.ts +17 -0
- package/dist/coding/registry/pnpm-lockfile.js +189 -0
- package/dist/coding/registry/pypi-plan.d.ts +50 -0
- package/dist/coding/registry/pypi-plan.js +406 -0
- package/dist/coding/registry/pypi-requirements.d.ts +58 -0
- package/dist/coding/registry/pypi-requirements.js +155 -0
- package/dist/coding/registry/pypi.d.ts +3 -27
- package/dist/coding/registry/pypi.js +40 -154
- package/dist/coding/registry/refusal-hints.d.ts +20 -0
- package/dist/coding/registry/refusal-hints.js +61 -0
- package/dist/coding/registry/refusal-wording.d.ts +11 -0
- package/dist/coding/registry/refusal-wording.js +56 -0
- package/dist/coding/registry/report.d.ts +70 -8
- package/dist/coding/registry/report.js +112 -5
- package/dist/coding/registry/types.d.ts +58 -8
- package/dist/coding/registry/yarn-lockfile.d.ts +30 -0
- package/dist/coding/registry/yarn-lockfile.js +367 -0
- package/dist/coding/services/catalog.d.ts +37 -351
- package/dist/coding/transcript/claude-code.d.ts +13 -0
- package/dist/coding/transcript/claude-code.js +273 -0
- package/dist/coding/transcript/download-protocol.d.ts +30 -0
- package/dist/coding/transcript/download-protocol.js +18 -0
- package/dist/coding/transcript/format.d.ts +20 -0
- package/dist/coding/transcript/format.js +32 -0
- package/dist/coding/transcript/recorder.d.ts +51 -0
- package/dist/coding/transcript/recorder.js +225 -0
- package/dist/coding/transcript/resume/checkout.d.ts +31 -0
- package/dist/coding/transcript/resume/checkout.js +203 -0
- package/dist/coding/transcript/resume/command.d.ts +17 -0
- package/dist/coding/transcript/resume/command.js +176 -0
- package/dist/coding/transcript/resume/download.d.ts +20 -0
- package/dist/coding/transcript/resume/download.js +153 -0
- package/dist/coding/transcript/resume/launch.d.ts +38 -0
- package/dist/coding/transcript/resume/launch.js +128 -0
- package/dist/coding/transcript/resume/session-file.d.ts +13 -0
- package/dist/coding/transcript/resume/session-file.js +79 -0
- package/dist/coding/transcript/resume/stand-in.d.ts +11 -0
- package/dist/coding/transcript/resume/stand-in.js +49 -0
- package/dist/coding-worker/artifact.d.ts +16 -3
- package/dist/coding-worker/artifact.js +5 -3
- package/dist/coding-worker/debug-trace.d.ts +6 -5
- package/dist/coding-worker/debug-trace.js +13 -18
- package/dist/coding-worker/dependency-cache.d.mts +229 -0
- package/dist/coding-worker/dependency-cache.mjs +2123 -0
- package/dist/coding-worker/driver.d.ts +4 -3
- package/dist/coding-worker/driver.js +118 -38
- package/dist/coding-worker/errors.d.ts +1 -0
- package/dist/coding-worker/errors.js +32 -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/task-note.d.ts +11 -0
- package/dist/coding-worker/task-note.js +13 -0
- package/dist/coding-worker/types.d.ts +13 -0
- package/dist/config/dependency-cache.d.ts +23 -0
- package/dist/config/dependency-cache.js +32 -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 +65 -3
- package/dist/config/providers.js +147 -11
- package/dist/config/test-simulators.d.ts +82 -0
- package/dist/config/test-simulators.js +305 -0
- package/dist/core/async-context.d.ts +2 -0
- package/dist/core/async-context.js +12 -0
- package/dist/core/budget-events.d.ts +108 -0
- package/dist/core/budget-events.js +346 -0
- package/dist/core/budget-groups.d.ts +50 -1
- package/dist/core/budget-groups.js +236 -27
- package/dist/core/budget-stopped-head.d.ts +46 -0
- package/dist/core/budget-stopped-head.js +81 -0
- package/dist/core/budget-topup.d.ts +51 -0
- package/dist/core/budget-topup.js +156 -0
- package/dist/core/budget-wording.d.ts +16 -2
- package/dist/core/budget-wording.js +40 -2
- package/dist/core/coding-queue.d.ts +2 -1
- package/dist/core/coding-queue.js +7 -0
- package/dist/core/cron.d.ts +19 -1
- package/dist/core/cron.js +17 -0
- package/dist/core/datastores.js +1 -1
- package/dist/core/db.d.ts +16 -6
- package/dist/core/db.js +12 -10
- package/dist/core/deferred-coding.d.ts +14 -0
- package/dist/core/deferred-coding.js +62 -0
- package/dist/core/delegation-child.d.ts +25 -0
- package/dist/core/delegation-child.js +37 -0
- package/dist/core/dispatch.d.ts +53 -0
- package/dist/core/dispatch.js +167 -57
- package/dist/core/engine-native.js +10 -6
- package/dist/core/error-code.d.ts +38 -0
- package/dist/core/error-code.js +168 -0
- package/dist/core/grants.d.ts +19 -6
- package/dist/core/grants.js +55 -6
- package/dist/core/host-events.d.ts +67 -5
- package/dist/core/host-events.js +571 -37
- package/dist/core/host-status.d.ts +85 -3
- package/dist/core/host-status.js +298 -20
- package/dist/core/issue-bridge.d.ts +50 -6
- package/dist/core/issue-bridge.js +151 -20
- package/dist/core/issue-events.d.ts +1 -1
- package/dist/core/issue-events.js +16 -11
- package/dist/core/issue-status.d.ts +1 -1
- package/dist/core/issue-status.js +68 -29
- package/dist/core/logger.d.ts +5 -0
- package/dist/core/logger.js +14 -1
- package/dist/core/lost-run-children.d.ts +6 -0
- package/dist/core/lost-run-children.js +32 -0
- package/dist/core/mark-ready.d.ts +34 -0
- package/dist/core/mark-ready.js +152 -0
- package/dist/core/markdown-code.d.ts +8 -0
- package/dist/core/markdown-code.js +107 -0
- package/dist/core/memory-tools.js +7 -0
- package/dist/core/merge-order-check.d.ts +130 -0
- package/dist/core/merge-order-check.js +343 -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 +19 -0
- package/dist/core/notification-templates.js +139 -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/pull-request-state-sync.js +2 -2
- package/dist/core/reconciler.d.ts +8 -5
- package/dist/core/reconciler.js +37 -6
- package/dist/core/related-pull-requests.d.ts +59 -7
- package/dist/core/related-pull-requests.js +140 -25
- package/dist/core/reply-parts.d.ts +21 -0
- package/dist/core/reply-parts.js +73 -0
- package/dist/core/repo-access.d.ts +15 -0
- package/dist/core/repo-access.js +66 -0
- package/dist/core/review-fix.d.ts +38 -1
- package/dist/core/review-fix.js +209 -7
- package/dist/core/review-host-checks.d.ts +1 -1
- package/dist/core/review-host-tools.d.ts +23 -0
- package/dist/core/review-host-tools.js +156 -14
- package/dist/core/review-reads.d.ts +55 -0
- package/dist/core/review-reads.js +233 -0
- package/dist/core/run-status.d.ts +12 -0
- package/dist/core/run-status.js +12 -0
- package/dist/core/runner.d.ts +384 -5
- package/dist/core/runner.js +1244 -372
- package/dist/core/schedule-start.d.ts +12 -0
- package/dist/core/schedule-start.js +14 -0
- package/dist/core/secret-broker-config.d.ts +39 -0
- package/dist/core/secret-broker-config.js +112 -0
- package/dist/core/secret-guard.d.ts +46 -0
- package/dist/core/secret-guard.js +104 -0
- package/dist/core/secret-guarded-stores.d.ts +12 -0
- package/dist/core/secret-guarded-stores.js +41 -0
- package/dist/core/secret-spans.d.ts +50 -0
- package/dist/core/secret-spans.js +94 -0
- package/dist/core/secrets.d.ts +39 -4
- package/dist/core/secrets.js +148 -40
- package/dist/core/stop-run.d.ts +46 -0
- package/dist/core/stop-run.js +61 -0
- 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 +94 -0
- package/dist/core/workflow-events.js +48 -0
- package/dist/core/workflow-run-events.d.ts +18 -0
- package/dist/core/workflow-run-events.js +75 -0
- package/dist/core/zod-json-schema.d.ts +4 -0
- package/dist/core/zod-json-schema.js +39 -0
- package/dist/core/zod3-window.d.ts +35 -0
- package/dist/core/zod3-window.js +124 -0
- package/dist/generated/prisma/browser.d.ts +92 -3
- package/dist/generated/prisma/client.d.ts +92 -3
- package/dist/generated/prisma/commonInputTypes.d.ts +364 -54
- package/dist/generated/prisma/enums.d.ts +37 -0
- package/dist/generated/prisma/enums.js +31 -0
- package/dist/generated/prisma/internal/class.d.ts +132 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +1332 -142
- package/dist/generated/prisma/internal/prismaNamespace.js +227 -5
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +234 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +227 -5
- package/dist/generated/prisma/models/Agent.d.ts +464 -7
- package/dist/generated/prisma/models/AgentRepository.d.ts +42 -1
- package/dist/generated/prisma/models/CodingAgentProfile.d.ts +301 -1
- package/dist/generated/prisma/models/CodingProxyRequest.d.ts +207 -3
- package/dist/generated/prisma/models/CodingProxySession.d.ts +114 -1
- package/dist/generated/prisma/models/CodingRun.d.ts +1841 -1
- package/dist/generated/prisma/models/DeferredReview.d.ts +78 -1
- package/dist/generated/prisma/models/DependencySnapshot.d.ts +1668 -0
- package/dist/generated/prisma/models/DependencySnapshot.js +1 -0
- package/dist/generated/prisma/models/IssuePullRequest.d.ts +6 -2
- package/dist/generated/prisma/models/LocalReview.d.ts +40 -1
- package/dist/generated/prisma/models/NativeGatewayCall.d.ts +1713 -0
- package/dist/generated/prisma/models/NativeGatewayCall.js +1 -0
- package/dist/generated/prisma/models/NativeGatewaySession.d.ts +1551 -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/RegistryAllowance.d.ts +41 -1
- package/dist/generated/prisma/models/RegistryApprovedVersion.d.ts +75 -1
- package/dist/generated/prisma/models/RegistryAuditRecord.d.ts +1364 -0
- package/dist/generated/prisma/models/RegistryAuditRecord.js +1 -0
- package/dist/generated/prisma/models/Run.d.ts +1619 -58
- package/dist/generated/prisma/models/RunBudgetEvent.d.ts +1211 -0
- package/dist/generated/prisma/models/RunBudgetEvent.js +1 -0
- package/dist/generated/prisma/models/RunHostCheck.d.ts +36 -1
- package/dist/generated/prisma/models/RunTranscript.d.ts +1339 -0
- package/dist/generated/prisma/models/RunTranscript.js +1 -0
- 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 +12 -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 +2584 -146
- package/dist/import/create.js +49 -3
- package/dist/import/neutral-schema.d.ts +50 -277
- package/dist/import/neutral-schema.js +7 -4
- package/dist/knowledge/check.d.ts +1 -1
- package/dist/knowledge/cli.d.ts +11 -1
- package/dist/knowledge/cli.js +37 -4
- package/dist/knowledge/concept.d.ts +3 -19
- package/dist/knowledge/location.d.ts +31 -0
- package/dist/knowledge/location.js +78 -0
- package/dist/mcp/auth/access.d.ts +11 -2
- package/dist/mcp/auth/access.js +15 -1
- package/dist/mcp/auth/ownership.d.ts +7 -7
- 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/auth/self-hosted/credentials.d.ts +3 -3
- package/dist/mcp/auth/self-hosted/session.d.ts +2 -2
- package/dist/mcp/context.d.ts +6 -0
- package/dist/mcp/host-events/github-ingress.d.ts +25 -1
- package/dist/mcp/host-events/github-ingress.js +70 -1
- package/dist/mcp/index.d.ts +19 -0
- package/dist/mcp/index.js +117 -20
- package/dist/mcp/server.d.ts +13 -0
- package/dist/mcp/server.js +33 -14
- package/dist/mcp/tools/agents.d.ts +4 -0
- package/dist/mcp/tools/agents.js +277 -24
- package/dist/mcp/tools/datastore.js +17 -8
- package/dist/mcp/tools/grants.js +9 -3
- package/dist/mcp/tools/memory.js +11 -1
- package/dist/mcp/tools/notification-channels.d.ts +2 -0
- package/dist/mcp/tools/notification-channels.js +293 -0
- package/dist/mcp/tools/packages.d.ts +2 -0
- package/dist/mcp/tools/packages.js +195 -0
- package/dist/mcp/tools/power.d.ts +2 -0
- package/dist/mcp/tools/power.js +77 -0
- package/dist/mcp/tools/repositories.js +18 -5
- package/dist/mcp/tools/run-access.d.ts +16 -0
- package/dist/mcp/tools/run-access.js +25 -0
- package/dist/mcp/tools/run-transcript-download.d.ts +30 -0
- package/dist/mcp/tools/run-transcript-download.js +129 -0
- package/dist/mcp/tools/run-transcript.d.ts +19 -0
- package/dist/mcp/tools/run-transcript.js +70 -0
- package/dist/mcp/tools/runs.d.ts +0 -6
- package/dist/mcp/tools/runs.js +41 -14
- package/dist/mcp/tools/scheduling.js +3 -1
- 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 +205 -10
- package/dist/mcp/tools/secret-elicitation-server.d.ts +6 -0
- package/dist/mcp/tools/secret-elicitation-server.js +10 -54
- 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 +186 -9
- package/dist/mcp/tools/stop-run.d.ts +2 -0
- package/dist/mcp/tools/stop-run.js +82 -0
- package/dist/mcp/tools/subagents.js +2 -2
- package/dist/mcp/tools/tools.d.ts +2 -2
- package/dist/mcp/tools/tools.js +47 -11
- package/dist/mcp/transport/stdio-link-host.d.ts +20 -0
- package/dist/mcp/transport/stdio-link-host.js +67 -0
- package/dist/mcp/transport/streamable-http.d.ts +13 -0
- package/dist/mcp/transport/streamable-http.js +41 -1
- 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 +110 -0
- package/dist/native-worker/docker-launcher.js +297 -0
- package/dist/native-worker/gateway-server.d.ts +77 -0
- package/dist/native-worker/gateway-server.js +412 -0
- package/dist/native-worker/gateway.d.ts +70 -0
- package/dist/native-worker/gateway.js +263 -0
- package/dist/native-worker/http-server.d.ts +24 -0
- package/dist/native-worker/http-server.js +113 -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 +106 -0
- package/dist/native-worker/kubernetes-launcher.js +375 -0
- package/dist/native-worker/launch.d.ts +62 -0
- package/dist/native-worker/launch.js +49 -0
- package/dist/native-worker/ledger.d.ts +125 -0
- package/dist/native-worker/ledger.js +245 -0
- package/dist/native-worker/loopback.d.ts +12 -0
- package/dist/native-worker/loopback.js +44 -0
- package/dist/native-worker/main.d.ts +8 -0
- package/dist/native-worker/main.js +85 -0
- package/dist/native-worker/protocol.d.ts +296 -0
- package/dist/native-worker/protocol.js +246 -0
- package/dist/native-worker/sandbox-executor.d.ts +83 -0
- package/dist/native-worker/sandbox-executor.js +284 -0
- package/dist/native-worker/stderr-redaction.d.ts +77 -0
- package/dist/native-worker/stderr-redaction.js +237 -0
- package/dist/native-worker/stdio.d.ts +29 -0
- package/dist/native-worker/stdio.js +189 -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 +141 -0
- package/dist/observability/metrics.js +6 -1
- 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 +14 -2
- 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/coding-proxy/dependency-cache/eviction.d.ts +29 -0
- package/dist/providers/coding-proxy/dependency-cache/eviction.js +87 -0
- package/dist/providers/coding-proxy/dependency-cache/key.d.ts +33 -0
- package/dist/providers/coding-proxy/dependency-cache/key.js +86 -0
- package/dist/providers/coding-proxy/dependency-cache/model-request-seal.d.ts +18 -0
- package/dist/providers/coding-proxy/dependency-cache/model-request-seal.js +40 -0
- package/dist/providers/coding-proxy/dependency-cache/prisma-store.d.ts +25 -0
- package/dist/providers/coding-proxy/dependency-cache/prisma-store.js +131 -0
- package/dist/providers/coding-proxy/dependency-cache/service.d.ts +70 -0
- package/dist/providers/coding-proxy/dependency-cache/service.js +409 -0
- package/dist/providers/coding-proxy/dependency-cache/store.d.ts +85 -0
- package/dist/providers/coding-proxy/dependency-cache/store.js +95 -0
- package/dist/providers/coding-proxy/hold.d.ts +32 -0
- package/dist/providers/coding-proxy/hold.js +86 -0
- package/dist/providers/coding-proxy/interrupted.d.ts +70 -0
- package/dist/providers/coding-proxy/interrupted.js +104 -0
- package/dist/providers/coding-proxy/memory-ledger.d.ts +15 -2
- package/dist/providers/coding-proxy/memory-ledger.js +84 -8
- package/dist/providers/coding-proxy/metering.d.ts +16 -0
- package/dist/providers/coding-proxy/metering.js +27 -4
- package/dist/providers/coding-proxy/prisma-ledger.d.ts +15 -2
- package/dist/providers/coding-proxy/prisma-ledger.js +179 -67
- package/dist/providers/coding-proxy/proxy.d.ts +14 -1
- package/dist/providers/coding-proxy/proxy.js +166 -14
- package/dist/providers/coding-proxy/registry/audit.d.ts +53 -10
- package/dist/providers/coding-proxy/registry/audit.js +94 -17
- package/dist/providers/coding-proxy/registry/lockfile-parse-worker.d.ts +36 -0
- package/dist/providers/coding-proxy/registry/lockfile-parse-worker.js +67 -0
- package/dist/providers/coding-proxy/registry/lockfile-parse.d.ts +16 -0
- package/dist/providers/coding-proxy/registry/lockfile-parse.js +110 -0
- package/dist/providers/coding-proxy/registry/plan.d.ts +9 -0
- package/dist/providers/coding-proxy/registry/plan.js +201 -48
- package/dist/providers/coding-proxy/registry/prisma-store.d.ts +11 -5
- package/dist/providers/coding-proxy/registry/prisma-store.js +120 -17
- package/dist/providers/coding-proxy/registry/service.d.ts +97 -16
- package/dist/providers/coding-proxy/registry/service.js +579 -112
- package/dist/providers/coding-proxy/registry/store.d.ts +84 -9
- package/dist/providers/coding-proxy/registry/store.js +54 -1
- package/dist/providers/coding-proxy/runtime.d.ts +4 -0
- package/dist/providers/coding-proxy/runtime.js +78 -16
- package/dist/providers/coding-proxy/server.d.ts +6 -1
- package/dist/providers/coding-proxy/server.js +45 -1
- package/dist/providers/coding-proxy/types.d.ts +65 -1
- package/dist/providers/datastore/types.d.ts +5 -0
- package/dist/providers/engine/types.d.ts +36 -2
- package/dist/providers/engine/types.js +39 -0
- package/dist/providers/executor/composition.d.ts +31 -1
- package/dist/providers/executor/composition.js +81 -8
- package/dist/providers/executor/container.d.ts +385 -9
- package/dist/providers/executor/container.js +1234 -99
- package/dist/providers/executor/dbos-status.d.ts +31 -0
- package/dist/providers/executor/dbos-status.js +64 -0
- package/dist/providers/executor/dbos.d.ts +91 -0
- package/dist/providers/executor/dbos.js +319 -26
- package/dist/providers/executor/deferred.d.ts +44 -0
- package/dist/providers/executor/deferred.js +100 -0
- package/dist/providers/executor/in-process.d.ts +9 -1
- package/dist/providers/executor/in-process.js +14 -4
- package/dist/providers/executor/linked-issue-reader.d.ts +16 -0
- package/dist/providers/executor/linked-issue-reader.js +30 -0
- package/dist/providers/executor/routing.d.ts +39 -10
- package/dist/providers/executor/routing.js +77 -32
- package/dist/providers/executor/types.d.ts +25 -0
- package/dist/providers/index.d.ts +2 -1
- package/dist/providers/issue-tracker/types.d.ts +17 -0
- package/dist/providers/issue-tracker/types.js +39 -1
- package/dist/providers/jobs/claude-tool-setup.d.ts +4 -0
- package/dist/providers/jobs/claude-tool-setup.js +5 -2
- package/dist/providers/jobs/docker-isolation.d.ts +8 -0
- package/dist/providers/jobs/docker-isolation.js +17 -0
- package/dist/providers/jobs/docker.d.ts +1 -0
- package/dist/providers/jobs/docker.js +29 -4
- package/dist/providers/jobs/fake-kubernetes-api.d.ts +23 -2
- package/dist/providers/jobs/fake-kubernetes-api.js +45 -1
- package/dist/providers/jobs/kubernetes-api.d.ts +25 -1
- package/dist/providers/jobs/kubernetes-api.js +17 -0
- package/dist/providers/jobs/kubernetes-client.d.ts +10 -1
- package/dist/providers/jobs/kubernetes-client.js +61 -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 +5 -1
- package/dist/providers/jobs/kubernetes-isolation.js +23 -1
- 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 +42 -2
- package/dist/providers/jobs/kubernetes.js +178 -19
- 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 +42 -0
- package/dist/providers/jobs/types.js +13 -1
- package/dist/providers/llm/anthropic.js +5 -1
- package/dist/providers/llm/bedrock.d.ts +0 -12
- package/dist/providers/llm/bedrock.js +4 -0
- package/dist/providers/llm/claude-messages.d.ts +3 -1
- package/dist/providers/llm/claude-messages.js +4 -1
- package/dist/providers/llm/claude-provider.d.ts +2 -2
- package/dist/providers/llm/claude-provider.js +2 -2
- package/dist/providers/llm/openai.d.ts +2 -2
- package/dist/providers/llm/openai.js +6 -2
- package/dist/providers/llm/routing.d.ts +2 -2
- package/dist/providers/llm/routing.js +2 -2
- package/dist/providers/llm/types.d.ts +12 -2
- package/dist/providers/review-host/github-events.d.ts +9 -1
- package/dist/providers/review-host/github-events.js +34 -12
- package/dist/providers/review-host/github.d.ts +39 -2
- package/dist/providers/review-host/github.js +274 -29
- package/dist/providers/review-host/local.d.ts +8 -4
- package/dist/providers/review-host/local.js +49 -31
- package/dist/providers/review-host/patch-pages.d.ts +41 -0
- package/dist/providers/review-host/patch-pages.js +176 -0
- package/dist/providers/review-host/review-format.d.ts +19 -1
- package/dist/providers/review-host/review-format.js +38 -7
- package/dist/providers/review-host/types.d.ts +157 -2
- package/dist/providers/review-host/types.js +8 -0
- package/dist/providers/storage/gcs.d.ts +63 -0
- package/dist/providers/storage/gcs.js +69 -0
- package/dist/providers/storage/index.d.ts +15 -0
- package/dist/providers/storage/index.js +14 -0
- package/dist/providers/storage/local.d.ts +24 -0
- package/dist/providers/storage/local.js +92 -0
- package/dist/providers/storage/meter.d.ts +14 -0
- package/dist/providers/storage/meter.js +34 -0
- package/dist/providers/storage/s3.d.ts +41 -0
- package/dist/providers/storage/s3.js +107 -0
- package/dist/providers/storage/types.d.ts +37 -3
- package/dist/providers/storage/types.js +21 -4
- package/dist/providers/vcs/git.d.ts +33 -2
- package/dist/providers/vcs/git.js +324 -22
- package/dist/providers/vcs/github-remote.d.ts +26 -3
- package/dist/providers/vcs/github-remote.js +124 -14
- package/dist/providers/vcs/github.d.ts +246 -12
- package/dist/providers/vcs/github.js +521 -61
- package/dist/providers/vcs/index.js +8 -1
- package/dist/providers/vcs/pr-description.d.ts +127 -0
- package/dist/providers/vcs/pr-description.js +270 -0
- package/dist/providers/vcs/remote.d.ts +33 -1
- package/dist/providers/vcs/routing.d.ts +4 -2
- package/dist/providers/vcs/routing.js +6 -0
- package/dist/providers/vcs/types.d.ts +80 -3
- package/dist/quickstart/coding-seed.d.ts +21 -3
- package/dist/quickstart/coding-seed.js +283 -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 +6 -6
- 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/generated/zod.bundle.js +62 -1
- package/dist/sandbox/generated/zod3.bundle.js +5 -0
- package/dist/sandbox/host-functions.d.ts +35 -0
- package/dist/sandbox/host-functions.js +150 -91
- 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 +161 -0
- package/dist/sandbox/tool-capabilities.d.ts +10 -30
- package/dist/sandbox/user-tool.d.ts +14 -0
- package/dist/sandbox/user-tool.js +32 -0
- package/dist/sandbox/zod-params.d.ts +37 -5
- package/dist/sandbox/zod-params.js +181 -26
- package/dist/serve.js +25 -0
- package/dist/viewer/api-schema.d.ts +372 -2454
- package/dist/viewer/api-schema.js +29 -0
- package/dist/viewer/build-schemas.js +10 -3
- 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 +170 -0
- package/dist/wardby-bin.js +10 -0
- package/docs/README.md +7 -0
- package/docs/agent-recipes.md +114 -27
- package/docs/architecture-runtime.md +16 -1
- package/docs/code-review-agents.md +689 -45
- package/docs/coding-agent-setup.md +602 -12
- package/docs/coding-packages.md +1037 -92
- package/docs/coding-services.md +30 -13
- package/docs/coding-worker-byo-images.md +218 -6
- package/docs/coding-worker-isolation.md +173 -44
- package/docs/getting-started-gke.md +332 -129
- package/docs/getting-started-identity-provider.md +31 -11
- package/docs/getting-started.md +14 -4
- package/docs/jira-agents.md +22 -9
- package/docs/knowledge/blob-not-found-semantics.md +41 -0
- package/docs/knowledge/bounded-file-descriptor.md +52 -0
- package/docs/knowledge/delegation-admission-slots.md +52 -0
- package/docs/knowledge/http-dispatcher-bootstrap.md +61 -0
- package/docs/knowledge/import-rename-target.md +52 -0
- package/docs/knowledge/index.md +57 -0
- package/docs/knowledge/log.md +9 -0
- package/docs/knowledge/mcp-cancelled-task-precedence.md +41 -0
- package/docs/knowledge/mcp-http-prompt-discovery.md +41 -0
- package/docs/knowledge/mcp-schema-module-boundary.md +41 -0
- package/docs/knowledge/nullable-tool-uniqueness.md +52 -0
- package/docs/knowledge/overview-cli.md +74 -0
- package/docs/knowledge/overview-coding-proxy.md +72 -0
- package/docs/knowledge/overview-coding.md +72 -0
- package/docs/knowledge/overview-config.md +60 -0
- package/docs/knowledge/overview-core.md +72 -0
- package/docs/knowledge/overview-deploy.md +83 -0
- package/docs/knowledge/overview-mcp.md +61 -0
- package/docs/knowledge/overview-prisma.md +72 -0
- package/docs/knowledge/overview-providers.md +89 -0
- package/docs/knowledge/overview-sandbox.md +83 -0
- package/docs/knowledge/overview-surfaces.md +83 -0
- package/docs/knowledge/private-datastore-shared-boundary.md +54 -0
- package/docs/knowledge/proxy-settlement-projections.md +41 -0
- package/docs/knowledge/run-pricing-first-writer.md +43 -0
- package/docs/knowledge/schedule-claim-transaction.md +41 -0
- package/docs/knowledge/secret-entry-replica-contract.md +52 -0
- package/docs/knowledge/terminal-run-first-writer.md +41 -0
- package/docs/knowledge/transactional-agent-authorization.md +52 -0
- package/docs/knowledge/viewer-notify-write-isolation.md +30 -0
- package/docs/knowledge/warm-input-publication.md +30 -0
- package/docs/knowledge/workspace-staging-failure.md +54 -0
- package/docs/knowledge.md +54 -14
- package/docs/native-sandbox.md +731 -0
- package/docs/observability.md +4 -0
- package/docs/release-verification.md +9 -0
- package/docs/security-deployment.md +146 -14
- package/docs/slack-notifications.md +268 -0
- package/docs/tool-secrets.md +284 -0
- package/docs/viewer-api.md +46 -5
- package/docs/viewer-api.openapi.yml +1404 -0
- package/help/admin-viewer.md +29 -3
- package/help/agent-recipes.md +27 -0
- package/help/architecture-agent.md +11 -6
- package/help/brokered-secrets.md +79 -0
- package/help/build-claude-code-image.md +131 -0
- package/help/build-worker-image.md +6 -7
- package/help/builder-agent.md +19 -6
- package/help/code-review-agents.md +243 -10
- package/help/coding-packages.md +156 -6
- package/help/cost-attribution.md +9 -5
- package/help/creating-agents.md +14 -1
- package/help/dependency-cache.md +143 -0
- package/help/deploy-gke.md +23 -9
- package/help/deployment-targets.md +25 -14
- package/help/errors/budget-exhausted.md +187 -0
- package/help/errors/budget-group-exhausted.md +5 -1
- package/help/errors/coding-provider-not-configured.md +2 -2
- package/help/errors/coding-stream-quota-exceeded.md +33 -0
- package/help/errors/coding-workspace-disk-exceeds-limit.md +39 -0
- package/help/errors/continuation-closed.md +9 -12
- package/help/errors/database-auth-invalid.md +28 -0
- package/help/errors/delegation-conflict.md +24 -0
- package/help/errors/file-list-truncated.md +45 -0
- package/help/errors/full-read-required.md +49 -0
- package/help/errors/github-account-not-allowed.md +29 -0
- package/help/errors/graph-incomplete.md +43 -0
- package/help/errors/graph-limit.md +43 -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/merge-conflict.md +49 -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-error.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-launch-abandoned.md +21 -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 +21 -0
- package/help/errors/native-sandbox-worker-lost.md +20 -0
- package/help/errors/native-sandbox-worker-unready.md +22 -0
- package/help/errors/package-not-allowed-pending.md +52 -0
- package/help/errors/package-not-allowed.md +51 -0
- package/help/errors/package-refusals.md +43 -0
- package/help/errors/patch-cursor-malformed.md +31 -0
- package/help/errors/run-lost.md +39 -0
- package/help/errors/run-transcript-download.md +44 -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-in-value.md +69 -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/errors/target-merge.md +40 -0
- package/help/fix-merge-conflicts.md +73 -0
- package/help/getting-started.md +4 -5
- package/help/github-app-setup.md +132 -0
- package/help/github.md +40 -1
- package/help/identity-and-access.md +7 -1
- package/help/jira.md +23 -6
- package/help/knowledge.md +16 -7
- package/help/local-repositories.md +3 -4
- package/help/low-power-mode.md +131 -0
- package/help/merge-order-check.md +122 -0
- package/help/mobile-app.md +54 -0
- package/help/native-capabilities.md +29 -1
- package/help/native-sandbox.md +150 -0
- package/help/observability.md +8 -0
- package/help/operating-agents.md +5 -0
- package/help/package-audit-mode.md +91 -0
- package/help/related-pull-requests.md +80 -16
- package/help/repo-instructions-and-skills.md +91 -0
- package/help/resume-runs-locally.md +131 -0
- package/help/review-fix-rounds.md +43 -1
- package/help/sharing-agents.md +59 -0
- package/help/slack-notifications.md +76 -0
- package/help/status-comments.md +31 -0
- package/help/stop-run.md +58 -0
- package/help/troubleshooting/budgets.md +28 -0
- package/help/troubleshooting/coding-workers.md +99 -7
- package/package.json +27 -13
- 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/migrations/20261016000000_registry_approved_accepted_advisories/migration.sql +3 -0
- package/prisma/migrations/20261016000001_mark_ready_when_passing/migration.sql +3 -0
- package/prisma/migrations/20261017000000_debug_trace_max_mb/migration.sql +4 -0
- package/prisma/migrations/20261018000000_dependency_cache/migration.sql +53 -0
- package/prisma/migrations/20261019000000_claude_max_output_tokens/migration.sql +5 -0
- package/prisma/migrations/20261021000000_pr_description_hash/migration.sql +4 -0
- package/prisma/migrations/20261022000000_dependency_snapshot_validation_metadata/migration.sql +4 -0
- package/prisma/migrations/20261023000000_package_audit_mode/migration.sql +27 -0
- package/prisma/migrations/20261024000000_budget_events/migration.sql +26 -0
- package/prisma/migrations/20261025000000_cache_aware_holds/migration.sql +9 -0
- package/prisma/migrations/20261026000000_budget_overrun/migration.sql +4 -0
- package/prisma/migrations/20261027000000_coding_run_request/migration.sql +1 -0
- package/prisma/migrations/20261028000000_native_gateway_review_reads/migration.sql +6 -0
- package/prisma/migrations/20261029000000_lead_survives_deploy/migration.sql +6 -0
- package/prisma/migrations/20261030000000_local_review_covered_sha/migration.sql +2 -0
- package/prisma/migrations/20261031000000_coding_run_budget_limit/migration.sql +9 -0
- package/prisma/migrations/20261101000000_run_transcripts/migration.sql +22 -0
- package/prisma/schema.prisma +631 -146
- package/dist/sandbox/generated/zod-to-json-schema.bundle.js +0 -5
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { isPackageAuditMode, mergePackageAuditReports, } from "../../coding/registry/audit-mode.js";
|
|
2
3
|
import { mkdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import { join, resolve, sep } from "node:path";
|
|
4
5
|
import { withBaseCommit } from "../../coding/base-commit.js";
|
|
6
|
+
import { withPullRequestDescription } from "../../coding/pr-description-note.js";
|
|
7
|
+
import { withLinkedIssue } from "../../coding/linked-issue-note.js";
|
|
8
|
+
import { buildClaudeContext, MAX_RECORDED_SKIPS } from "../../coding/claude-context.js";
|
|
5
9
|
import { normalizeCollectExclusions } from "../../coding/collect-exclude.js";
|
|
6
10
|
import { codingProviderForModelProvider } from "../../coding/provider.js";
|
|
7
11
|
import { CodingProfileSchema } from "../../coding/profile.js";
|
|
@@ -9,29 +13,50 @@ import { parseStoredServices, storedServiceNames, workerServices } from "../../c
|
|
|
9
13
|
import { MAX_SERVICE_DECLARATION_BYTES, SERVICE_DECLARATION_PATH } from "../../coding/services/declaration.js";
|
|
10
14
|
import { SERVICE_UNREADY_CATEGORY, SERVICE_UNREADY_ERROR, serviceUnreadyName, serviceUnreadySentence, } from "../../coding/services/wording.js";
|
|
11
15
|
import { PROTECTED_PATH_CATEGORY, protectedPathFromError, protectedPathSentence, } from "../../coding/protected-path-wording.js";
|
|
12
|
-
import { CONTINUATION_CLOSED_CATEGORY, CONTINUATION_CLOSED_ERROR } from "../../coding/continuation-wording.js";
|
|
13
|
-
import {
|
|
16
|
+
import { budgetStoppedSummary, CONTINUATION_CLOSED_CATEGORY, CONTINUATION_CLOSED_ERROR, WORKER_BUDGET_EXHAUSTED_SUMMARY, } from "../../coding/continuation-wording.js";
|
|
17
|
+
import { CONFLICT_MARKERS_ERROR_PREFIX, MERGE_CONFLICT_CATEGORY, TARGET_MERGE_CATEGORY, TARGET_MERGE_FAILED_ERROR, conflictMarkersPathFromError, conflictMarkersSentence, mergeSentence, withTargetMerge, } from "../../coding/merge-wording.js";
|
|
18
|
+
import { budgetSentence, overrunSentence } from "../../core/budget-wording.js";
|
|
14
19
|
import { fileSelfDefect } from "../../core/self-defects.js";
|
|
15
20
|
import { isTransactionUnavailable } from "../../core/dispatch.js";
|
|
21
|
+
import { topUpCodingBudget } from "../../core/budget-topup.js";
|
|
16
22
|
import { CONTENDED_TX_MAX_WAIT_MS } from "../../core/timing.js";
|
|
17
23
|
import { classifyProviderFailure, providerClassOfCategory, providerSentence, } from "../../core/provider-wording.js";
|
|
18
24
|
import { logger } from "../../core/logger.js";
|
|
19
25
|
import { requiredLevel } from "../../core/repo-access.js";
|
|
20
26
|
import { ensurePrivateDirectory } from "../../core/private-directory.js";
|
|
21
27
|
import { codingRunObserver } from "../../coding/observability.js";
|
|
22
|
-
import { CODING_PROTOCOL_VERSION, CodingRunResultSchema, CodingTaskInputSchema,
|
|
28
|
+
import { CODING_PROTOCOL_VERSION, CodingRunResultSchema, CodingTaskInputSchema, MAX_CODING_INPUT_BYTES, isLocalRepository, parseCodingWorkerOutputJson, redactAndTruncate, restoredDependencyFolders, sanitizeDependencyCacheReport, } from "../../coding/protocol.js";
|
|
23
29
|
import { parseStoredEntry } from "../llm/catalog-types.js";
|
|
24
30
|
import { resolveCodingEntry } from "../../core/run-pricing.js";
|
|
25
31
|
import { CODING_PROXY_ALIAS, CODING_PROXY_PORT, isImmutableDockerImage } from "../jobs/docker-isolation.js";
|
|
26
32
|
import { normalizeRegistryLockfiles } from "../../coding/registry/lockfiles.js";
|
|
27
|
-
import {
|
|
33
|
+
import { warningRefusals } from "../../coding/registry/report.js";
|
|
34
|
+
import { hostBudgetSentence, loadBudgetEvent, ownerBudgetSentence, proxyRefusalDetails, recordBudgetEvent, } from "../../core/budget-events.js";
|
|
35
|
+
import { JobCapacityError, } from "../jobs/types.js";
|
|
36
|
+
import { ISSUE_KEY, issueReference } from "../issue-tracker/types.js";
|
|
28
37
|
import { HEARTBEAT_TIMEOUT_MS } from "../../core/timing.js";
|
|
29
38
|
import { collectRelatedPullRequests } from "../../core/related-pull-requests.js";
|
|
39
|
+
import { isIssueMentionTask } from "../../core/host-status.js";
|
|
40
|
+
import { pullRequestBody, } from "../vcs/github.js";
|
|
41
|
+
import { closingIssueNumber, descriptionSha256 } from "../vcs/pr-description.js";
|
|
30
42
|
const containerLog = logger.child({ module: "container-executor" });
|
|
43
|
+
/** A transcript read and store must not hold up the job's removal for long. */
|
|
44
|
+
const TRANSCRIPT_RECORD_TIMEOUT_MS = 60_000;
|
|
45
|
+
/** A logged context skip path is cut to this many characters: a refused import is reported as written. */
|
|
46
|
+
const MAX_LOGGED_SKIP_PATH_CHARS = 256;
|
|
31
47
|
/** The input path in a spec built only to ask the launcher about capacity; never written or read. */
|
|
32
48
|
const CAPACITY_PROBE_INPUT = "/dev/null";
|
|
33
49
|
const TERMINAL_STATUSES = new Set(["succeeded", "failed", "refused", "lost", "budget_exhausted", "cancelled"]);
|
|
34
50
|
const PROVISIONING_BACKEND = "provisioning";
|
|
51
|
+
/** The longest a no-change fix round's description refresh may hold up the run's terminal write. */
|
|
52
|
+
export const DESCRIPTION_REFRESH_TIMEOUT_MS = 60_000;
|
|
53
|
+
/**
|
|
54
|
+
* After the cluster refuses a run's launch for capacity, how long this executor leaves the run
|
|
55
|
+
* waiting in the queue before trying it again (drainCodingQueue starts it every tick meanwhile).
|
|
56
|
+
*/
|
|
57
|
+
export const CAPACITY_RETRY_BACKOFF_MS = 60_000;
|
|
58
|
+
/** How many runs above a root coding run are searched for the mention that started it. */
|
|
59
|
+
const MAX_LINKED_ISSUE_RUN_DEPTH = 8;
|
|
35
60
|
/** Serializes concurrency-slot claims across replicas. Distinct from the OAuth client lock (7412901). */
|
|
36
61
|
const CODING_SLOT_LOCK_SQL = "SELECT 1 AS locked FROM pg_advisory_xact_lock(7412902)";
|
|
37
62
|
/**
|
|
@@ -45,6 +70,14 @@ const CODING_SLOT_LOCK_SQL = "SELECT 1 AS locked FROM pg_advisory_xact_lock(7412
|
|
|
45
70
|
*/
|
|
46
71
|
class RunNoLongerActiveError extends Error {
|
|
47
72
|
}
|
|
73
|
+
/** Private sentinel: rolls back requeueAfterCapacityRefusal's transaction with the outcome it carries. */
|
|
74
|
+
class NotRequeuedError extends Error {
|
|
75
|
+
outcome;
|
|
76
|
+
constructor(outcome) {
|
|
77
|
+
super(outcome);
|
|
78
|
+
this.outcome = outcome;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
48
81
|
function proxyProtocol(provider) {
|
|
49
82
|
if (provider === "codex")
|
|
50
83
|
return "openai-responses";
|
|
@@ -65,6 +98,51 @@ export async function normalizeCollectedLockfiles(provider, workspacePath) {
|
|
|
65
98
|
proxyBaseUrl: `http://${CODING_PROXY_ALIAS}:${CODING_PROXY_PORT}`,
|
|
66
99
|
});
|
|
67
100
|
}
|
|
101
|
+
/** The worker's dependency cache report, as CodingRunRecord columns: nothing on a run without one. */
|
|
102
|
+
function dependencyCacheRecord(output) {
|
|
103
|
+
return output.dependencyCache ? { dependencyCacheResult: output.dependencyCache } : {};
|
|
104
|
+
}
|
|
105
|
+
/** What a push records on the run: its base commit, and a pushed branch's name (a local repository). */
|
|
106
|
+
function pushedRecord(finalized) {
|
|
107
|
+
return {
|
|
108
|
+
baseSha: finalized.baseCommit,
|
|
109
|
+
...(finalized.outcome === "branch_pushed" ? { resultBranch: finalized.headRef } : {}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** The PR body's dependency cache line: the folders restored from a snapshot (hit or nearest). */
|
|
113
|
+
function restoredDependenciesFor(output) {
|
|
114
|
+
const folders = restoredDependencyFolders(output.dependencyCache);
|
|
115
|
+
return folders.length > 0 ? { restoredDependencies: folders } : {};
|
|
116
|
+
}
|
|
117
|
+
/** The stored form of a description outcome: written, replaced, refreshed:<reason>, unchanged, edited_by_hand, skipped:<reason>. */
|
|
118
|
+
export function descriptionOutcomeLabel(outcome) {
|
|
119
|
+
if (outcome.kind === "refreshed" || outcome.kind === "skipped")
|
|
120
|
+
return `${outcome.kind}:${outcome.reason}`;
|
|
121
|
+
return outcome.kind;
|
|
122
|
+
}
|
|
123
|
+
/** A local repository has no pull request: its runs get no description note and record no description outcome. */
|
|
124
|
+
function hasPullRequestDescription(run) {
|
|
125
|
+
return !isLocalRepository(run.repository);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* A budget-stopped run's stored result names what its kept changes became
|
|
129
|
+
* (see keepStoppedChanges): the `changesPushed` detail for its status comment,
|
|
130
|
+
* or nothing when it pushed nothing.
|
|
131
|
+
*/
|
|
132
|
+
function storedChangesPushed(result) {
|
|
133
|
+
const parsed = CodingRunResultSchema.safeParse(result);
|
|
134
|
+
if (!parsed.success)
|
|
135
|
+
return {};
|
|
136
|
+
const stored = parsed.data;
|
|
137
|
+
if (stored.outcome === "branch_pushed")
|
|
138
|
+
return { changesPushed: {} };
|
|
139
|
+
if (stored.outcome === "pull_request_opened" || stored.outcome === "pull_request_updated") {
|
|
140
|
+
return { changesPushed: { pullRequestNumber: stored.pullRequestNumber } };
|
|
141
|
+
}
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
/** The longest reading the linked issue may hold up a continuation's launch. */
|
|
145
|
+
export const LINKED_ISSUE_READ_TIMEOUT_MS = 15_000;
|
|
68
146
|
export class PrismaContainerExecutionStore {
|
|
69
147
|
db;
|
|
70
148
|
options;
|
|
@@ -93,11 +171,13 @@ export class PrismaContainerExecutionStore {
|
|
|
93
171
|
return null;
|
|
94
172
|
return {
|
|
95
173
|
runId: row.id,
|
|
174
|
+
agentId: row.agentId,
|
|
96
175
|
status: row.status,
|
|
97
176
|
agentKind: row.agent.kind,
|
|
98
177
|
agentName: row.agent.name,
|
|
99
178
|
ownerId: row.agent.ownerId,
|
|
100
179
|
task: row.codingRun.task,
|
|
180
|
+
...(row.codingRun.request ? { request: row.codingRun.request } : {}),
|
|
101
181
|
repository: row.codingRun.repository,
|
|
102
182
|
baseRef: row.codingRun.baseRef,
|
|
103
183
|
headRef: row.codingRun.headRef,
|
|
@@ -109,8 +189,11 @@ export class PrismaContainerExecutionStore {
|
|
|
109
189
|
rootCodingRunId: row.codingRun.rootCodingRunId,
|
|
110
190
|
issueProvider: row.codingRun.issueProvider,
|
|
111
191
|
issueKey: row.codingRun.issueKey,
|
|
192
|
+
mergeOrder: row.codingRun.mergeOrder,
|
|
112
193
|
budgetUsd: Number(row.codingRun.budgetReservedUsd),
|
|
113
194
|
agentBudgetUsd: Number(row.agent.budgetUsd),
|
|
195
|
+
...(row.codingRun.budgetBaseUsd == null ? {} : { budgetBaseUsd: Number(row.codingRun.budgetBaseUsd) }),
|
|
196
|
+
...(row.codingRun.budgetLimitUsd == null ? {} : { budgetLimitUsd: Number(row.codingRun.budgetLimitUsd) }),
|
|
114
197
|
budgetGroupName: row.agent.budgetGroup?.name ?? null,
|
|
115
198
|
tokensIn: row.tokensIn,
|
|
116
199
|
tokensOut: row.tokensOut,
|
|
@@ -125,7 +208,13 @@ export class PrismaContainerExecutionStore {
|
|
|
125
208
|
toolImage: row.codingRun.toolImage,
|
|
126
209
|
workspaceDiskMb: row.codingRun.workspaceDiskMb,
|
|
127
210
|
debugTrace: row.codingRun.debugTrace,
|
|
211
|
+
debugTraceMaxMb: row.codingRun.debugTraceMaxMb,
|
|
128
212
|
maxTurns: row.codingRun.maxTurns,
|
|
213
|
+
maxOutputTokens: row.codingRun.maxOutputTokens,
|
|
214
|
+
repoSkills: row.codingRun.repoSkills,
|
|
215
|
+
claudeBareMode: row.codingRun.claudeBareMode,
|
|
216
|
+
dependencyCache: row.codingRun.dependencyCache,
|
|
217
|
+
sessionResume: row.codingRun.sessionResume,
|
|
129
218
|
services: row.codingRun.services,
|
|
130
219
|
failureCategory: row.codingRun.failureCategory,
|
|
131
220
|
profileRepository: row.agent.codingProfile?.repository ?? null,
|
|
@@ -141,8 +230,111 @@ export class PrismaContainerExecutionStore {
|
|
|
141
230
|
repository: pr.repository,
|
|
142
231
|
number: pr.number,
|
|
143
232
|
...(pr.state ? { state: pr.state } : {}),
|
|
233
|
+
...(pr.mergeOrder !== undefined ? { mergeOrder: pr.mergeOrder } : {}),
|
|
144
234
|
}));
|
|
145
235
|
}
|
|
236
|
+
async pullRequestMarkedReady(rootRunId) {
|
|
237
|
+
const root = await this.db.codingRun.findUnique({ where: { runId: rootRunId }, select: { markedReadyAt: true } });
|
|
238
|
+
return Boolean(root?.markedReadyAt);
|
|
239
|
+
}
|
|
240
|
+
async pullRequestDescriptionBaseline(rootRunId) {
|
|
241
|
+
const root = await this.db.codingRun.findUnique({
|
|
242
|
+
where: { runId: rootRunId },
|
|
243
|
+
select: {
|
|
244
|
+
prDescriptionSha256: true,
|
|
245
|
+
prDescriptionRunId: true,
|
|
246
|
+
result: true,
|
|
247
|
+
repository: true,
|
|
248
|
+
baseRef: true,
|
|
249
|
+
headRef: true,
|
|
250
|
+
dependencyCacheResult: true,
|
|
251
|
+
issueProvider: true,
|
|
252
|
+
issueKey: true,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
if (!root)
|
|
256
|
+
throw new Error("coding_run_not_found");
|
|
257
|
+
// Legacy pull requests (no recorded author run) carry the root run's own text.
|
|
258
|
+
const agentTextRunId = root.prDescriptionRunId ?? rootRunId;
|
|
259
|
+
const source = agentTextRunId === rootRunId
|
|
260
|
+
? root.result
|
|
261
|
+
: (await this.db.codingRun.findUnique({ where: { runId: agentTextRunId }, select: { result: true } }))?.result;
|
|
262
|
+
const previousAgentText = storedSummary(source);
|
|
263
|
+
const rootSummary = storedSummary(root.result);
|
|
264
|
+
const legacySource = root.prDescriptionSha256 === null
|
|
265
|
+
? {
|
|
266
|
+
repository: root.repository,
|
|
267
|
+
baseRef: root.baseRef,
|
|
268
|
+
headRef: root.headRef,
|
|
269
|
+
...(rootSummary !== undefined ? { summary: rootSummary } : {}),
|
|
270
|
+
...(storedField(root.result, "tests") !== undefined ? { tests: storedField(root.result, "tests") } : {}),
|
|
271
|
+
...(root.dependencyCacheResult != null ? { dependencyCache: root.dependencyCacheResult } : {}),
|
|
272
|
+
issueProvider: root.issueProvider,
|
|
273
|
+
issueKey: root.issueKey,
|
|
274
|
+
}
|
|
275
|
+
: undefined;
|
|
276
|
+
return {
|
|
277
|
+
recordedSha256: root.prDescriptionSha256,
|
|
278
|
+
...(previousAgentText !== undefined ? { previousAgentText } : {}),
|
|
279
|
+
agentTextRunId,
|
|
280
|
+
...(legacySource ? { legacySource } : {}),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async linkedIssueSource(rootRunId) {
|
|
284
|
+
const root = await this.db.codingRun.findUnique({
|
|
285
|
+
where: { runId: rootRunId },
|
|
286
|
+
select: { issueProvider: true, issueKey: true, run: { select: { parentRunId: true } } },
|
|
287
|
+
});
|
|
288
|
+
if (!root)
|
|
289
|
+
throw new Error("coding_run_not_found");
|
|
290
|
+
// The mention that started the work is on the root's delegating run, or a run above it.
|
|
291
|
+
let hostStatus = null;
|
|
292
|
+
let parentRunId = root.run.parentRunId;
|
|
293
|
+
for (let depth = 0; parentRunId && depth < MAX_LINKED_ISSUE_RUN_DEPTH; depth++) {
|
|
294
|
+
const parent = await this.db.run.findUnique({
|
|
295
|
+
where: { id: parentRunId },
|
|
296
|
+
select: {
|
|
297
|
+
parentRunId: true,
|
|
298
|
+
taskOverride: true,
|
|
299
|
+
hostStatus: { select: { provider: true, repository: true, number: true } },
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
if (!parent)
|
|
303
|
+
break;
|
|
304
|
+
if (parent.hostStatus) {
|
|
305
|
+
const { provider, repository, number } = parent.hostStatus;
|
|
306
|
+
hostStatus = {
|
|
307
|
+
provider,
|
|
308
|
+
repository,
|
|
309
|
+
number,
|
|
310
|
+
issueMention: provider === "github" && isIssueMentionTask(parent.taskOverride, repository, number),
|
|
311
|
+
};
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
parentRunId = parent.parentRunId;
|
|
315
|
+
}
|
|
316
|
+
return { issueProvider: root.issueProvider, issueKey: root.issueKey, hostStatus };
|
|
317
|
+
}
|
|
318
|
+
async pullRequestRunIds(rootRunId) {
|
|
319
|
+
const rows = await this.db.codingRun.findMany({
|
|
320
|
+
where: { OR: [{ runId: rootRunId }, { rootCodingRunId: rootRunId }] },
|
|
321
|
+
select: { runId: true, result: true },
|
|
322
|
+
orderBy: { run: { startedAt: "asc" } },
|
|
323
|
+
});
|
|
324
|
+
const pushed = rows.filter((row) => {
|
|
325
|
+
const outcome = storedField(row.result, "outcome");
|
|
326
|
+
return outcome === "pull_request_opened" || outcome === "pull_request_updated";
|
|
327
|
+
});
|
|
328
|
+
// The root first, whatever the clock says.
|
|
329
|
+
return [...pushed.filter((row) => row.runId === rootRunId), ...pushed.filter((row) => row.runId !== rootRunId)].map((row) => row.runId);
|
|
330
|
+
}
|
|
331
|
+
async recordPullRequestDescription(input) {
|
|
332
|
+
const written = await this.db.codingRun.updateMany({
|
|
333
|
+
where: { runId: input.rootRunId, prDescriptionSha256: input.expectedSha256 },
|
|
334
|
+
data: { prDescriptionSha256: input.sha256, prDescriptionRunId: input.agentTextRunId },
|
|
335
|
+
});
|
|
336
|
+
return written.count === 1;
|
|
337
|
+
}
|
|
146
338
|
async claimProvisioning(runId, claimId) {
|
|
147
339
|
try {
|
|
148
340
|
return await this.db.$transaction(async (tx) => {
|
|
@@ -232,6 +424,52 @@ export class PrismaContainerExecutionStore {
|
|
|
232
424
|
data: { queuedAt: new Date() },
|
|
233
425
|
});
|
|
234
426
|
}
|
|
427
|
+
async requeueAfterCapacityRefusal(runId, owned) {
|
|
428
|
+
const holds = [
|
|
429
|
+
...(owned.claimId ? [{ jobBackend: PROVISIONING_BACKEND, jobHandle: owned.claimId }] : []),
|
|
430
|
+
...(owned.planned ? [{ jobBackend: owned.planned.backend, jobHandle: owned.planned.id }] : []),
|
|
431
|
+
];
|
|
432
|
+
if (holds.length === 0)
|
|
433
|
+
return "not_owned";
|
|
434
|
+
try {
|
|
435
|
+
return await this.db.$transaction(async (tx) => {
|
|
436
|
+
const run = await tx.run.findUnique({ where: { id: runId }, select: { startedAt: true } });
|
|
437
|
+
if (!run)
|
|
438
|
+
return "not_owned";
|
|
439
|
+
// queuedAt is the dispatch time, not now: claimProvisioning clears it on every claim, and a
|
|
440
|
+
// fresh value per refusal would keep a run the cluster refuses forever from timing out.
|
|
441
|
+
const released = await tx.codingRun.updateMany({
|
|
442
|
+
where: { runId, OR: holds, run: { status: { in: ["pending", "running"] } } },
|
|
443
|
+
data: { jobBackend: null, jobHandle: null, queuedAt: run.startedAt },
|
|
444
|
+
});
|
|
445
|
+
if (released.count !== 1)
|
|
446
|
+
return "not_owned";
|
|
447
|
+
// The pod never started, so its session served no request. A run has one session, and the
|
|
448
|
+
// next attempt creates its own: drop this one, but never one with spend recorded against it.
|
|
449
|
+
const session = await tx.codingProxySession.findUnique({
|
|
450
|
+
where: { runId },
|
|
451
|
+
select: { id: true, _count: { select: { requests: true } } },
|
|
452
|
+
});
|
|
453
|
+
if (session && session._count.requests > 0)
|
|
454
|
+
throw new NotRequeuedError("spend_recorded");
|
|
455
|
+
if (session)
|
|
456
|
+
await tx.codingProxySession.delete({ where: { id: session.id } });
|
|
457
|
+
const waiting = await tx.run.updateMany({
|
|
458
|
+
where: { id: runId, status: { in: ["pending", "running"] } },
|
|
459
|
+
data: { status: "pending", heartbeatAt: null },
|
|
460
|
+
});
|
|
461
|
+
// Ended (stopped, say) since the release above: roll the release back too.
|
|
462
|
+
if (waiting.count !== 1)
|
|
463
|
+
throw new NotRequeuedError("not_owned");
|
|
464
|
+
return "requeued";
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
catch (err) {
|
|
468
|
+
if (err instanceof NotRequeuedError)
|
|
469
|
+
return err.outcome;
|
|
470
|
+
throw err;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
235
473
|
async persistHandle(runId, claimId, handle) {
|
|
236
474
|
await this.db.$transaction(async (tx) => {
|
|
237
475
|
const coding = await tx.codingRun.findUnique({ where: { runId }, include: { run: true } });
|
|
@@ -283,6 +521,11 @@ export class PrismaContainerExecutionStore {
|
|
|
283
521
|
resultSchema: CODING_PROTOCOL_VERSION,
|
|
284
522
|
...(record.resultBranch ? { resultBranch: record.resultBranch } : {}),
|
|
285
523
|
...(record.baseSha ? { baseSha: record.baseSha } : {}),
|
|
524
|
+
...(record.dependencyCacheResult ? { dependencyCacheResult: record.dependencyCacheResult } : {}),
|
|
525
|
+
...(record.prDescriptionOutcome ? { prDescriptionOutcome: record.prDescriptionOutcome } : {}),
|
|
526
|
+
...(record.prDescriptionSha256 && record.prDescriptionRunId
|
|
527
|
+
? { prDescriptionSha256: record.prDescriptionSha256, prDescriptionRunId: record.prDescriptionRunId }
|
|
528
|
+
: {}),
|
|
286
529
|
},
|
|
287
530
|
});
|
|
288
531
|
await tx.run.update({
|
|
@@ -301,8 +544,33 @@ export class PrismaContainerExecutionStore {
|
|
|
301
544
|
await this.selfDefect({ ...finished, status, error: null, finishedAt });
|
|
302
545
|
}
|
|
303
546
|
}
|
|
547
|
+
async recordBudgetRefusal(runId, sessionId) {
|
|
548
|
+
const session = await this.db.codingProxySession.findUnique({
|
|
549
|
+
where: { id: sessionId },
|
|
550
|
+
select: { budgetRefusal: true },
|
|
551
|
+
});
|
|
552
|
+
// The proxy (a less-trusted role) wrote this: keep only well-formed amounts, model and turn.
|
|
553
|
+
const refusal = proxyRefusalDetails(session?.budgetRefusal);
|
|
554
|
+
if (!refusal)
|
|
555
|
+
return;
|
|
556
|
+
const existing = await this.db.runBudgetEvent.findFirst({
|
|
557
|
+
where: { runId, details: { path: ["source"], equals: "proxy" } },
|
|
558
|
+
select: { id: true },
|
|
559
|
+
});
|
|
560
|
+
if (existing)
|
|
561
|
+
return;
|
|
562
|
+
const coding = await this.db.codingRun.findUnique({ where: { runId }, select: { budgetLimitedBy: true } });
|
|
563
|
+
await recordBudgetEvent(this.db, runId, (coding?.budgetLimitedBy ?? "run"), refusal);
|
|
564
|
+
}
|
|
565
|
+
budgetEvent(runId) {
|
|
566
|
+
return loadBudgetEvent(this.db, runId);
|
|
567
|
+
}
|
|
568
|
+
topUpBudget(runId) {
|
|
569
|
+
return topUpCodingBudget(this.db, runId);
|
|
570
|
+
}
|
|
304
571
|
async terminate(runId, status, error, audit) {
|
|
305
572
|
const finishedAt = new Date();
|
|
573
|
+
let changed = false;
|
|
306
574
|
const agentId = await this.db.$transaction(async (tx) => {
|
|
307
575
|
const updated = await tx.run.updateMany({
|
|
308
576
|
where: { id: runId, status: { in: ["pending", "running"] } },
|
|
@@ -310,6 +578,7 @@ export class PrismaContainerExecutionStore {
|
|
|
310
578
|
});
|
|
311
579
|
if (!updated.count)
|
|
312
580
|
return null;
|
|
581
|
+
changed = true;
|
|
313
582
|
if (audit) {
|
|
314
583
|
await tx.codingRun.update({
|
|
315
584
|
where: { runId },
|
|
@@ -324,6 +593,7 @@ export class PrismaContainerExecutionStore {
|
|
|
324
593
|
// After commit, so the failure category is readable; cancelled/refused are skipped inside fileSelfDefect.
|
|
325
594
|
if (agentId)
|
|
326
595
|
await this.selfDefect({ id: runId, agentId, status, error, finishedAt });
|
|
596
|
+
return changed;
|
|
327
597
|
}
|
|
328
598
|
}
|
|
329
599
|
/** Plain capabilities live only for the short provisioning window. */
|
|
@@ -344,6 +614,9 @@ export class RunCapabilityVault {
|
|
|
344
614
|
this.values.delete(runId);
|
|
345
615
|
}
|
|
346
616
|
}
|
|
617
|
+
/** A run's deduplicated served packages and refusals (see summarizeRegistryFetches), and its package audit. */
|
|
618
|
+
/** The poll interval, at most, while a run with an overrun limit is near its grant. */
|
|
619
|
+
export const TOP_UP_NEAR_POLL_MS = 1_000;
|
|
347
620
|
class PreflightError extends Error {
|
|
348
621
|
}
|
|
349
622
|
/** A refusal by repository authorization; its message is the failure category's prefix (repo_access_*). */
|
|
@@ -357,6 +630,11 @@ export class ContainerExecutor {
|
|
|
357
630
|
now;
|
|
358
631
|
observer;
|
|
359
632
|
startedAt = new Map();
|
|
633
|
+
/**
|
|
634
|
+
* When the cluster last refused each queued run's launch for capacity (this.now() ms). Per
|
|
635
|
+
* process: another replica's drain may start the run again inside the backoff.
|
|
636
|
+
*/
|
|
637
|
+
capacityRefusedAt = new Map();
|
|
360
638
|
constructor(options) {
|
|
361
639
|
this.options = options;
|
|
362
640
|
this.artifactRoot = resolve(options.artifactRoot);
|
|
@@ -408,6 +686,9 @@ export class ContainerExecutor {
|
|
|
408
686
|
const current = this.active.get(runId);
|
|
409
687
|
if (current)
|
|
410
688
|
return current;
|
|
689
|
+
// Refused for capacity a moment ago: keep its place and clock in the queue, untouched.
|
|
690
|
+
if (this.inCapacityBackoff(runId))
|
|
691
|
+
return;
|
|
411
692
|
this.startedAt.set(runId, this.now().getTime());
|
|
412
693
|
this.emit({ stage: "queued", runId });
|
|
413
694
|
const execution = this.execute(runId).finally(async () => {
|
|
@@ -427,10 +708,16 @@ export class ContainerExecutor {
|
|
|
427
708
|
return;
|
|
428
709
|
if (TERMINAL_STATUSES.has(run.status))
|
|
429
710
|
return this.cleanupTerminal(run);
|
|
430
|
-
this.emit({ stage: "stopping", runId, jobId: run.jobHandle?.id });
|
|
431
711
|
// Persist the cancellation fence first so a concurrent finisher cannot publish.
|
|
432
712
|
const failure = this.failure("cancelled", { cancelled: true });
|
|
433
|
-
|
|
713
|
+
// False when the run reached a terminal status after the load above: its finisher owns the
|
|
714
|
+
// cleanup and the continuation comment (recover() cleans up any terminal run the reconciler sees
|
|
715
|
+
// later), so stopping now would overwrite a good outcome. "stopping" is emitted only once this
|
|
716
|
+
// stop owns the run, so it is always followed by its terminal and cleanup events.
|
|
717
|
+
if (!(await this.options.store.terminate(runId, "cancelled", failure.error, failure.audit)))
|
|
718
|
+
return;
|
|
719
|
+
this.emit({ stage: "stopping", runId, jobId: run.jobHandle?.id });
|
|
720
|
+
await this.recordRefusal(runId, run.proxySessionId);
|
|
434
721
|
this.terminal(run, "cancelled", failure.audit);
|
|
435
722
|
if (run.proxySessionId)
|
|
436
723
|
await this.options.sessions.cancelSession(run.proxySessionId).catch(() => undefined);
|
|
@@ -441,10 +728,11 @@ export class ContainerExecutor {
|
|
|
441
728
|
this.options.capabilities.delete(runId);
|
|
442
729
|
const input = this.preflightForCleanup(run);
|
|
443
730
|
const workspace = input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null;
|
|
444
|
-
if (workspace)
|
|
731
|
+
if (workspace)
|
|
445
732
|
await this.options.vcs.cleanup(workspace).catch(() => undefined);
|
|
446
|
-
|
|
447
|
-
|
|
733
|
+
const target = workspace ?? this.continuationTargetOf(run);
|
|
734
|
+
if (target)
|
|
735
|
+
await this.options.vcs.notifyContinuationFinished?.(target, "failed", { agentName: run.agentName });
|
|
448
736
|
await rm(this.artifactPath(runId), { recursive: true, force: true }).catch(() => undefined);
|
|
449
737
|
this.emit({ stage: "cleanup", runId, jobId: run.jobHandle?.id, cleanupSucceeded: true });
|
|
450
738
|
}
|
|
@@ -472,6 +760,9 @@ export class ContainerExecutor {
|
|
|
472
760
|
await this.abandon(run, "coding_job_status_unavailable");
|
|
473
761
|
return { state: "lost", reason: "coding_job_status_unavailable" };
|
|
474
762
|
}
|
|
763
|
+
if (status.state === "pending" && status.capacityRefused && this.options.store.requeueAfterCapacityRefusal) {
|
|
764
|
+
return this.finishCapacityRequeue(run, run.jobHandle);
|
|
765
|
+
}
|
|
475
766
|
if (status.state === "pending" || status.state === "running") {
|
|
476
767
|
await this.options.store.heartbeat(run.runId);
|
|
477
768
|
return { state: "active" };
|
|
@@ -492,9 +783,12 @@ export class ContainerExecutor {
|
|
|
492
783
|
let sessionId = run.proxySessionId;
|
|
493
784
|
let handle = run.jobHandle;
|
|
494
785
|
let claimId = null;
|
|
786
|
+
let planned;
|
|
495
787
|
let spendEnabled = Boolean(sessionId);
|
|
496
788
|
try {
|
|
497
|
-
|
|
789
|
+
// A launched run resumed after a restart is checked the way recover() checks it: its cost so
|
|
790
|
+
// far may be past the limit (a reply can cost more than its hold), and it is still delivered.
|
|
791
|
+
const prepared = this.preflight(run, { launched: Boolean(handle) });
|
|
498
792
|
if (!handle) {
|
|
499
793
|
if (run.provisioningClaim)
|
|
500
794
|
return;
|
|
@@ -545,14 +839,14 @@ export class ContainerExecutor {
|
|
|
545
839
|
spendEnabled = true;
|
|
546
840
|
sessionId = session.id;
|
|
547
841
|
this.options.capabilities.set(runId, session.capability);
|
|
548
|
-
const inputArtifact = await this.writeInput(run, deadlineAt, workspace
|
|
842
|
+
const inputArtifact = await this.writeInput(run, deadlineAt, workspace);
|
|
549
843
|
const beforeLaunch = await this.requireCurrent(runId);
|
|
550
844
|
if (TERMINAL_STATUSES.has(beforeLaunch.status))
|
|
551
845
|
throw new Error("coding_run_no_longer_active");
|
|
552
846
|
const spec = this.jobSpec(run, inputArtifact);
|
|
553
847
|
// Backends whose handle is derivable (Kubernetes) persist it first, so a crash between the
|
|
554
848
|
// launch and the write still leaves a handle `abandon()` can stop and remove the run with.
|
|
555
|
-
|
|
849
|
+
planned = this.options.jobs.plannedHandle?.(spec);
|
|
556
850
|
if (planned)
|
|
557
851
|
await this.options.store.persistHandle(runId, claimId, planned);
|
|
558
852
|
handle = await this.whileHeartbeating(runId, () => this.options.jobs.launch(spec));
|
|
@@ -560,12 +854,15 @@ export class ContainerExecutor {
|
|
|
560
854
|
throw new Error("coding_job_handle_mismatch");
|
|
561
855
|
}
|
|
562
856
|
await this.options.store.persistHandle(runId, claimId, handle);
|
|
857
|
+
this.capacityRefusedAt.delete(runId);
|
|
563
858
|
this.options.capabilities.delete(runId);
|
|
564
859
|
this.emit({ stage: "launched", runId, jobId: handle.id, budgetReservedUsd: run.budgetUsd });
|
|
565
860
|
}
|
|
566
861
|
let delay = this.options.pollMinMs ?? 250;
|
|
567
862
|
const maximumDelay = this.options.pollMaxMs ?? 5_000;
|
|
568
863
|
let observedRunning = false;
|
|
864
|
+
// Overrun from available room: asked on each poll until it is granted in full or refused once.
|
|
865
|
+
let asksForTopUps = run.budgetLimitUsd !== undefined && run.budgetLimitUsd > run.budgetUsd;
|
|
569
866
|
for (;;) {
|
|
570
867
|
const status = await this.options.jobs.status(handle);
|
|
571
868
|
if (status.state !== "pending" && status.state !== "running") {
|
|
@@ -577,6 +874,17 @@ export class ContainerExecutor {
|
|
|
577
874
|
this.emit({ stage: "running", runId, jobId: handle.id });
|
|
578
875
|
}
|
|
579
876
|
await this.options.store.heartbeat(runId);
|
|
877
|
+
if (asksForTopUps && this.options.store.topUpBudget) {
|
|
878
|
+
const topUp = await this.options.store.topUpBudget(runId).catch((error) => {
|
|
879
|
+
containerLog.warn({ runId, error: safeError(error) }, "coding run top-up failed; asking again next poll");
|
|
880
|
+
return { outcome: "not_needed", near: true };
|
|
881
|
+
});
|
|
882
|
+
if (topUp.outcome === "unavailable" || topUp.outcome === "closed")
|
|
883
|
+
asksForTopUps = false;
|
|
884
|
+
// Near its grant: poll often, so the next step lands before a request no longer fits.
|
|
885
|
+
else if (topUp.near)
|
|
886
|
+
delay = Math.min(delay, TOP_UP_NEAR_POLL_MS);
|
|
887
|
+
}
|
|
580
888
|
await this.sleep(delay);
|
|
581
889
|
delay = Math.min(maximumDelay, Math.max(delay + 1, Math.ceil(delay * 1.5)));
|
|
582
890
|
run = (await this.options.store.load(runId)) ?? run;
|
|
@@ -586,6 +894,34 @@ export class ContainerExecutor {
|
|
|
586
894
|
}
|
|
587
895
|
catch (error) {
|
|
588
896
|
this.options.capabilities.delete(runId);
|
|
897
|
+
if (error instanceof JobCapacityError && claimId && this.options.store.requeueAfterCapacityRefusal) {
|
|
898
|
+
// The cluster has no room for the pod (Kubernetes: the namespace quota): the run waits in
|
|
899
|
+
// the queue again rather than failing. The launcher already removed what it created, so
|
|
900
|
+
// there is no job to stop; nothing ran, so the session is dropped with the claim and a
|
|
901
|
+
// fresh one is made on the next attempt.
|
|
902
|
+
if (sessionId)
|
|
903
|
+
await this.options.sessions.cancelSession(sessionId).catch(() => undefined);
|
|
904
|
+
const requeued = await this.options.store.requeueAfterCapacityRefusal(runId, {
|
|
905
|
+
claimId,
|
|
906
|
+
...(planned ? { planned } : {}),
|
|
907
|
+
});
|
|
908
|
+
if (requeued !== "spend_recorded") {
|
|
909
|
+
if (workspace)
|
|
910
|
+
await this.options.vcs.cleanup(workspace).catch(() => undefined);
|
|
911
|
+
if (requeued === "requeued") {
|
|
912
|
+
// The next attempt writes its own input; this one names a session that is gone.
|
|
913
|
+
await rm(this.artifactPath(runId), { recursive: true, force: true }).catch(() => undefined);
|
|
914
|
+
this.recordCapacityRefusal(runId);
|
|
915
|
+
this.emit({ stage: "queued", runId });
|
|
916
|
+
containerLog.info({ runId }, "coding run waiting for capacity: the cluster refused its pod");
|
|
917
|
+
}
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
// Spend against a pod that never started: fail it like any launch error, and stop and remove
|
|
921
|
+
// the launcher's record through the planned handle so the run is never launched again.
|
|
922
|
+
if (planned)
|
|
923
|
+
handle = planned;
|
|
924
|
+
}
|
|
589
925
|
if (sessionId)
|
|
590
926
|
await this.options.sessions.cancelSession(sessionId).catch(() => undefined);
|
|
591
927
|
if (handle)
|
|
@@ -596,7 +932,8 @@ export class ContainerExecutor {
|
|
|
596
932
|
: !spendEnabled && error instanceof PreflightError
|
|
597
933
|
? "refused"
|
|
598
934
|
: "failed";
|
|
599
|
-
|
|
935
|
+
await this.recordRefusal(runId, sessionId);
|
|
936
|
+
const failure = outOfBudget ? await this.budgetFailure(runId, error) : this.failure(error);
|
|
600
937
|
this.emit({
|
|
601
938
|
stage: "stopping",
|
|
602
939
|
runId,
|
|
@@ -615,7 +952,7 @@ export class ContainerExecutor {
|
|
|
615
952
|
await this.options.vcs.cleanup(workspace).catch(() => undefined);
|
|
616
953
|
await this.options.vcs.notifyContinuationFinished?.(workspace, outOfBudget ? "budget_exhausted" : "failed", {
|
|
617
954
|
agentName: run.agentName,
|
|
618
|
-
...(outOfBudget ? { budgetSentence: this.budgetSentence(run) } : {}),
|
|
955
|
+
...(outOfBudget ? { budgetSentence: await this.budgetSentence(run) } : {}),
|
|
619
956
|
...(unreadyService ? { serviceSentence: serviceUnreadySentence([unreadyService]) } : {}),
|
|
620
957
|
...(protectedPath ? { protectedPathSentence: protectedPathSentence(protectedPath) } : {}),
|
|
621
958
|
});
|
|
@@ -680,42 +1017,72 @@ export class ContainerExecutor {
|
|
|
680
1017
|
if (!decision.ok)
|
|
681
1018
|
throw new RepoAccessError(`repo_access_${decision.reason}`);
|
|
682
1019
|
}
|
|
683
|
-
/**
|
|
684
|
-
|
|
1020
|
+
/**
|
|
1021
|
+
* The CodingRun's recorded issue as the Related section shows it (a Jira or
|
|
1022
|
+
* GitHub issue): control-plane data from the CodingRun row, key re-validated.
|
|
1023
|
+
*/
|
|
1024
|
+
recordedIssue(run) {
|
|
685
1025
|
const { issueProvider, issueKey } = run;
|
|
686
|
-
if (!issueProvider || !issueKey
|
|
1026
|
+
if (!issueProvider || !issueKey)
|
|
1027
|
+
return undefined;
|
|
1028
|
+
return issueReference(issueProvider, issueKey, (key) => this.options.issueUrl?.(issueProvider, key));
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* The originating tracker issue for the PR title/body. A GitHub issue is not
|
|
1032
|
+
* one: its line comes from issueSourceFor (`From issue #N`).
|
|
1033
|
+
*/
|
|
1034
|
+
issueFor(run) {
|
|
1035
|
+
if (run.issueProvider === "github")
|
|
687
1036
|
return {};
|
|
688
|
-
|
|
1037
|
+
const issue = this.recordedIssue(run);
|
|
1038
|
+
return issue ? { issue } : {};
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* The PR title/body's originating issue, whichever system it is in: the
|
|
1042
|
+
* CodingRun's tracker issue (issueFor), else the GitHub issue in the same
|
|
1043
|
+
* repository whose mention started the run tree (`From issue #N`). Read from
|
|
1044
|
+
* the root run, so a continuation's rebuilt body keeps the root's line.
|
|
1045
|
+
* Never throws: a failed lookup gives no issue line.
|
|
1046
|
+
*/
|
|
1047
|
+
async issueSourceFor(run) {
|
|
1048
|
+
const tracker = this.issueFor(run);
|
|
1049
|
+
if (tracker.issue || !this.options.store.linkedIssueSource)
|
|
1050
|
+
return tracker;
|
|
689
1051
|
try {
|
|
690
|
-
|
|
1052
|
+
const status = (await this.options.store.linkedIssueSource(run.rootCodingRunId ?? run.runId)).hostStatus;
|
|
1053
|
+
if (status?.provider === "github" &&
|
|
1054
|
+
status.issueMention === true &&
|
|
1055
|
+
status.repository.toLowerCase() === run.repository.toLowerCase() &&
|
|
1056
|
+
Number.isSafeInteger(status.number) &&
|
|
1057
|
+
status.number > 0) {
|
|
1058
|
+
return { issueNumber: status.number };
|
|
1059
|
+
}
|
|
691
1060
|
}
|
|
692
|
-
catch {
|
|
693
|
-
|
|
1061
|
+
catch (err) {
|
|
1062
|
+
containerLog.warn({ err, runId: run.runId }, "could not find the issue the run came from");
|
|
694
1063
|
}
|
|
695
|
-
|
|
696
|
-
? ISSUE_TRACKER_NAMES[issueProvider]
|
|
697
|
-
: undefined;
|
|
698
|
-
return {
|
|
699
|
-
issue: {
|
|
700
|
-
key: issueKey,
|
|
701
|
-
...(url?.startsWith("https://") ? { url } : {}),
|
|
702
|
-
...(trackerName ? { trackerName } : {}),
|
|
703
|
-
},
|
|
704
|
-
};
|
|
1064
|
+
return {};
|
|
705
1065
|
}
|
|
706
1066
|
/** The new PR's Related pull requests input: earlier PRs of the same request, then this one. Never throws. */
|
|
707
1067
|
async relatedFor(run) {
|
|
708
|
-
// A continuation
|
|
1068
|
+
// A continuation's Related block is preserved from the current body (the description update keeps it).
|
|
709
1069
|
if (run.rootCodingRunId || !this.options.store.relatedPullRequests)
|
|
710
1070
|
return {};
|
|
711
1071
|
try {
|
|
712
1072
|
const earlier = await this.options.store.relatedPullRequests(run.runId);
|
|
713
1073
|
if (earlier.length === 0)
|
|
714
1074
|
return {};
|
|
715
|
-
const
|
|
1075
|
+
const issue = this.recordedIssue(run);
|
|
716
1076
|
return {
|
|
717
1077
|
related: {
|
|
718
|
-
entries: [
|
|
1078
|
+
entries: [
|
|
1079
|
+
...earlier,
|
|
1080
|
+
{
|
|
1081
|
+
repository: run.repository,
|
|
1082
|
+
self: true,
|
|
1083
|
+
...(run.mergeOrder != null ? { mergeOrder: run.mergeOrder } : {}),
|
|
1084
|
+
},
|
|
1085
|
+
],
|
|
719
1086
|
...(issue ? { issue } : {}),
|
|
720
1087
|
},
|
|
721
1088
|
};
|
|
@@ -739,14 +1106,20 @@ export class ContainerExecutor {
|
|
|
739
1106
|
const entry = resolveCodingEntry(run.provider, run.model);
|
|
740
1107
|
return { version: entry.priceVersion, entry: parseStoredEntry(entry) };
|
|
741
1108
|
}
|
|
742
|
-
|
|
1109
|
+
/**
|
|
1110
|
+
* `launched`: the run's job was already launched (it is running, or done and
|
|
1111
|
+
* only its push is left). The cost check applies only before launch: one
|
|
1112
|
+
* reply can cost more than its hold, so a launched run may already be past
|
|
1113
|
+
* its limit, and its finished work is still delivered.
|
|
1114
|
+
*/
|
|
1115
|
+
preflight(run, options = {}) {
|
|
743
1116
|
try {
|
|
744
1117
|
if (run.status !== "pending" && run.status !== "running")
|
|
745
1118
|
throw new Error("coding_run_status_invalid");
|
|
746
1119
|
if (run.agentKind !== "coding" || !run.ownerId)
|
|
747
1120
|
throw new Error("coding_run_ownership_invalid");
|
|
748
1121
|
this.modelTerms(run);
|
|
749
|
-
if (!Number.isFinite(run.budgetUsd) || run.budgetUsd <= 0 || run.costUsd > run.budgetUsd) {
|
|
1122
|
+
if (!Number.isFinite(run.budgetUsd) || run.budgetUsd <= 0 || (!options.launched && run.costUsd > run.budgetUsd)) {
|
|
750
1123
|
throw new Error("coding_run_budget_invalid");
|
|
751
1124
|
}
|
|
752
1125
|
const profile = CodingProfileSchema.parse({
|
|
@@ -795,11 +1168,19 @@ export class ContainerExecutor {
|
|
|
795
1168
|
// "failed" and is only flipped right before a success or out-of-budget return.
|
|
796
1169
|
let outcome = "failed";
|
|
797
1170
|
let finishedSummary;
|
|
1171
|
+
// Set with a success that spent past the run's base budget; says so on the host.
|
|
1172
|
+
let overrun = {};
|
|
798
1173
|
// Set when a failed job's session relayed a model-provider failure; names it on the host.
|
|
799
1174
|
// Only a failed job: a transient provider error the worker retried past did not end the run.
|
|
800
1175
|
let providerClass;
|
|
801
1176
|
// Set when finalizeChanges refused the collected diff for touching a protected path; names it on the host.
|
|
802
1177
|
let protectedPath;
|
|
1178
|
+
// Set when a continuation's merge of its target branch left conflict markers behind; names the file on the host.
|
|
1179
|
+
let conflictMarkersPath;
|
|
1180
|
+
// Set with a success whose push merged the pull request's target branch; says what on the host.
|
|
1181
|
+
let merged = {};
|
|
1182
|
+
// Set when a run stopped at its budget still pushed its changes so far; says so on the host.
|
|
1183
|
+
let changesPushed;
|
|
803
1184
|
try {
|
|
804
1185
|
if (jobState !== "succeeded") {
|
|
805
1186
|
const collected = await this.options.jobs.collect(handle).catch(() => null);
|
|
@@ -808,7 +1189,8 @@ export class ContainerExecutor {
|
|
|
808
1189
|
// The worker usually fails outright when the proxy refuses a model
|
|
809
1190
|
// request for budget; the session records that refusal.
|
|
810
1191
|
if (await this.outOfBudget(sessionId)) {
|
|
811
|
-
|
|
1192
|
+
await this.recordRefusal(run.runId, sessionId);
|
|
1193
|
+
const failure = await this.budgetFailure(run.runId, `job_${reason}`, { issues: collected?.diagnosticIssues });
|
|
812
1194
|
await this.options.store.terminate(run.runId, "budget_exhausted", failure.error, failure.audit);
|
|
813
1195
|
this.terminal(run, "budget_exhausted", failure.audit);
|
|
814
1196
|
outcome = "budget_exhausted";
|
|
@@ -820,6 +1202,7 @@ export class ContainerExecutor {
|
|
|
820
1202
|
? this.providerFailureOf(run, provider, `job_${reason}`, { issues: collected?.diagnosticIssues })
|
|
821
1203
|
: this.failure(`job_${reason}`, { issues: collected?.diagnosticIssues });
|
|
822
1204
|
providerClass = provider?.class;
|
|
1205
|
+
await this.recordRefusal(run.runId, sessionId);
|
|
823
1206
|
await this.options.store.terminate(run.runId, status, failure.error, failure.audit);
|
|
824
1207
|
this.terminal(run, status, failure.audit);
|
|
825
1208
|
return;
|
|
@@ -828,98 +1211,547 @@ export class ContainerExecutor {
|
|
|
828
1211
|
this.emit({ stage: "collected", runId: run.runId, jobId: handle.id });
|
|
829
1212
|
if (collected.reason !== "completed" || !collected.resultArtifact)
|
|
830
1213
|
throw new Error("coding_result_missing");
|
|
831
|
-
const output =
|
|
1214
|
+
const output = parseCodingWorkerOutputJson(collected.resultArtifact);
|
|
832
1215
|
if (output.runId !== run.runId)
|
|
833
1216
|
throw new Error("coding_result_run_mismatch");
|
|
834
|
-
|
|
835
|
-
|
|
1217
|
+
const current = this.requireActive(await this.requireCurrent(run.runId));
|
|
1218
|
+
// Finished work is delivered even when it cost more than the run's limit (one reply
|
|
1219
|
+
// can cost more than its hold); the status comment then says so (overrunDetails).
|
|
1220
|
+
if (output.outcome === "budget_exhausted") {
|
|
836
1221
|
this.emit({ stage: "budget_cutoff", runId: run.runId, jobId: handle.id });
|
|
837
|
-
await this.
|
|
838
|
-
this.
|
|
1222
|
+
const kept = await this.keepStoppedChanges(run, handle, current, output, existingWorkspace);
|
|
1223
|
+
await this.recordRefusal(run.runId, sessionId);
|
|
1224
|
+
if (kept) {
|
|
1225
|
+
await this.options.store.complete(run.runId, "budget_exhausted", kept.result, kept.record);
|
|
1226
|
+
this.emit({ stage: kept.finalized.outcome, runId: run.runId, jobId: handle.id });
|
|
1227
|
+
this.terminal(kept.current, "budget_exhausted");
|
|
1228
|
+
changesPushed =
|
|
1229
|
+
kept.finalized.outcome === "branch_pushed" ? {} : { pullRequestNumber: kept.finalized.pullRequestNumber };
|
|
1230
|
+
if (kept.finalized.outcome === "pull_request_updated" && kept.finalized.targetMerge) {
|
|
1231
|
+
merged = { mergeSentence: mergeSentence(kept.finalized.targetMerge) };
|
|
1232
|
+
}
|
|
1233
|
+
finishedSummary = output.summary;
|
|
1234
|
+
}
|
|
1235
|
+
else {
|
|
1236
|
+
const latest = await this.requireCurrent(run.runId).catch(() => current);
|
|
1237
|
+
await this.options.store.complete(run.runId, "budget_exhausted", this.resultFor(output, latest, "budget_exhausted"), dependencyCacheRecord(output));
|
|
1238
|
+
this.terminal(latest, "budget_exhausted");
|
|
1239
|
+
}
|
|
839
1240
|
outcome = "budget_exhausted";
|
|
840
1241
|
return;
|
|
841
1242
|
}
|
|
842
|
-
|
|
843
|
-
|
|
1243
|
+
// A workspace that merged its target branch has that merge to push even when the agent changed nothing.
|
|
1244
|
+
if (output.outcome === "no_changes" && !(await this.mergedTargetBranch(current, existingWorkspace))) {
|
|
1245
|
+
// Before complete: the terminal hook (a no-change fix round's re-review) reads the refreshed body.
|
|
1246
|
+
const description = await this.refreshDescriptionInTime(current, output, existingWorkspace);
|
|
1247
|
+
await this.recordRefusal(run.runId, sessionId);
|
|
1248
|
+
await this.options.store.complete(run.runId, "succeeded", this.resultFor(output, current), {
|
|
1249
|
+
...dependencyCacheRecord(output),
|
|
1250
|
+
...description,
|
|
1251
|
+
});
|
|
844
1252
|
this.terminal(current, "succeeded");
|
|
845
1253
|
outcome = "succeeded";
|
|
1254
|
+
overrun = this.overrunDetails(current);
|
|
846
1255
|
finishedSummary = output.summary;
|
|
847
1256
|
return;
|
|
848
1257
|
}
|
|
849
|
-
const
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
await this.options.store.complete(run.runId, "budget_exhausted", this.resultFor(output, current, "budget_exhausted"));
|
|
859
|
-
this.terminal(current, "budget_exhausted");
|
|
860
|
-
outcome = "budget_exhausted";
|
|
861
|
-
return;
|
|
862
|
-
}
|
|
863
|
-
// One more (usually cached) check right before anything is pushed: access
|
|
864
|
-
// revoked while the run was working stops the push.
|
|
865
|
-
await this.authorizeRepository(current, { inFlight: true });
|
|
866
|
-
const report = await this.loadRegistryReport(run.runId);
|
|
867
|
-
const finalized = await this.options.vcs.finalizeChanges(workspace, {
|
|
868
|
-
summary: output.summary,
|
|
869
|
-
tests: output.tests,
|
|
870
|
-
tag: output.tag,
|
|
871
|
-
...report,
|
|
872
|
-
...this.issueFor(current),
|
|
873
|
-
...(await this.relatedFor(current)),
|
|
874
|
-
});
|
|
1258
|
+
const pushed = await this.pushChanges(run, handle, current, output, output.summary, existingWorkspace);
|
|
1259
|
+
const { finalized, workspace } = pushed;
|
|
1260
|
+
// An empty diff pushes nothing, so it refreshes the description the way a
|
|
1261
|
+
// worker no_changes does, before complete (the terminal hook reads the body).
|
|
1262
|
+
const description = finalized.outcome === "no_changes"
|
|
1263
|
+
? await this.refreshDescriptionInTime(pushed.current, output, workspace)
|
|
1264
|
+
: await this.recordDescription(pushed.current, pushed.descriptionState, finalized.outcome === "pull_request_opened" || finalized.outcome === "pull_request_updated"
|
|
1265
|
+
? finalized.description
|
|
1266
|
+
: undefined);
|
|
875
1267
|
// A pushed branch (local repository) is a successful run with no pull
|
|
876
1268
|
// request; its branch is recorded on the run for the operator to merge.
|
|
877
|
-
const result = this.resultFor(output, current, finalized.outcome, finalized);
|
|
1269
|
+
const result = this.resultFor(output, pushed.current, finalized.outcome, finalized);
|
|
1270
|
+
await this.recordRefusal(run.runId, sessionId);
|
|
878
1271
|
await this.options.store.complete(run.runId, "succeeded", result, {
|
|
879
|
-
|
|
880
|
-
...(finalized
|
|
1272
|
+
...dependencyCacheRecord(output),
|
|
1273
|
+
...pushedRecord(finalized),
|
|
1274
|
+
...description,
|
|
881
1275
|
});
|
|
882
|
-
if (finalized.outcome
|
|
883
|
-
finalized.outcome === "pull_request_updated" ||
|
|
884
|
-
finalized.outcome === "branch_pushed") {
|
|
1276
|
+
if (finalized.outcome !== "no_changes") {
|
|
885
1277
|
this.emit({ stage: finalized.outcome, runId: run.runId, jobId: handle.id });
|
|
886
1278
|
}
|
|
887
|
-
this.terminal(current, "succeeded");
|
|
1279
|
+
this.terminal(pushed.current, "succeeded");
|
|
888
1280
|
outcome = "succeeded";
|
|
1281
|
+
overrun = this.overrunDetails(pushed.current);
|
|
1282
|
+
if (finalized.outcome === "pull_request_updated" && finalized.targetMerge) {
|
|
1283
|
+
merged = { mergeSentence: mergeSentence(finalized.targetMerge) };
|
|
1284
|
+
}
|
|
889
1285
|
finishedSummary = output.summary;
|
|
890
1286
|
}
|
|
891
1287
|
catch (error) {
|
|
892
|
-
if (
|
|
893
|
-
|
|
1288
|
+
if (error instanceof Error && error.message === "coding_run_no_longer_active") {
|
|
1289
|
+
// Stopped while its result was collected: the stop already wrote the terminal row.
|
|
1290
|
+
containerLog.info({ runId: run.runId }, "coding run was stopped before its result was published");
|
|
1291
|
+
}
|
|
1292
|
+
else if (await this.outOfBudget(sessionId)) {
|
|
1293
|
+
await this.recordRefusal(run.runId, sessionId);
|
|
1294
|
+
const failure = await this.budgetFailure(run.runId, error);
|
|
894
1295
|
await this.options.store.terminate(run.runId, "budget_exhausted", failure.error, failure.audit);
|
|
895
1296
|
this.terminal(run, "budget_exhausted", failure.audit);
|
|
896
1297
|
outcome = "budget_exhausted";
|
|
897
1298
|
}
|
|
898
1299
|
else {
|
|
1300
|
+
await this.recordRefusal(run.runId, sessionId);
|
|
899
1301
|
const failure = this.failure(error);
|
|
900
1302
|
await this.options.store.terminate(run.runId, "failed", failure.error, failure.audit);
|
|
901
1303
|
this.terminal(run, "failed", failure.audit);
|
|
902
1304
|
protectedPath = protectedPathFromError(error) ?? undefined;
|
|
1305
|
+
conflictMarkersPath = conflictMarkersPathFromError(error) ?? undefined;
|
|
903
1306
|
}
|
|
904
1307
|
}
|
|
905
1308
|
finally {
|
|
1309
|
+
if (run.sessionResume)
|
|
1310
|
+
await this.recordTranscript(run, handle);
|
|
906
1311
|
await this.options.jobs.remove(handle).catch(() => undefined);
|
|
907
1312
|
const input = this.preflightForCleanup(run);
|
|
908
1313
|
const workspace = existingWorkspace ?? (input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null);
|
|
909
|
-
if (workspace)
|
|
1314
|
+
if (workspace)
|
|
910
1315
|
await this.options.vcs.cleanup(workspace).catch(() => undefined);
|
|
911
|
-
|
|
1316
|
+
// A run another process launched has no workspace here; its pull request still gets the final status.
|
|
1317
|
+
const target = workspace ?? this.continuationTargetOf(run);
|
|
1318
|
+
if (target) {
|
|
1319
|
+
await this.options.vcs.notifyContinuationFinished?.(target, outcome, {
|
|
912
1320
|
summary: finishedSummary,
|
|
913
1321
|
agentName: run.agentName,
|
|
914
|
-
...(outcome === "
|
|
1322
|
+
...(outcome === "succeeded" ? { ...merged, ...overrun } : {}),
|
|
1323
|
+
...(outcome === "budget_exhausted" ? { budgetSentence: await this.budgetSentence(run) } : {}),
|
|
1324
|
+
...(outcome === "budget_exhausted" && changesPushed ? { changesPushed, ...merged } : {}),
|
|
915
1325
|
...(outcome === "failed" && providerClass ? { providerSentence: providerSentence(providerClass) } : {}),
|
|
916
1326
|
...(outcome === "failed" && protectedPath
|
|
917
1327
|
? { protectedPathSentence: protectedPathSentence(protectedPath) }
|
|
918
1328
|
: {}),
|
|
1329
|
+
...(outcome === "failed" && conflictMarkersPath
|
|
1330
|
+
? { conflictMarkersSentence: conflictMarkersSentence(conflictMarkersPath) }
|
|
1331
|
+
: {}),
|
|
919
1332
|
});
|
|
920
1333
|
}
|
|
921
1334
|
await rm(this.artifactPath(run.runId), { recursive: true, force: true }).catch(() => undefined);
|
|
922
1335
|
this.emit({ stage: "cleanup", runId: run.runId, jobId: handle.id, cleanupSucceeded: true });
|
|
1336
|
+
await this.notifyTerminal(run.runId);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
/** Whether the run's workspace merged its pull request's target branch (PreparedWorkspace.targetMerge). */
|
|
1340
|
+
async mergedTargetBranch(run, existingWorkspace) {
|
|
1341
|
+
if (!run.rootCodingRunId)
|
|
1342
|
+
return false;
|
|
1343
|
+
if (existingWorkspace)
|
|
1344
|
+
return Boolean(existingWorkspace.targetMerge);
|
|
1345
|
+
try {
|
|
1346
|
+
return Boolean((await this.options.vcs.recoverWorkspace(this.preflight(run, { launched: true })))?.targetMerge);
|
|
1347
|
+
}
|
|
1348
|
+
catch {
|
|
1349
|
+
return false;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Pushes a finished job's changes: copies the job's workspace into the run's,
|
|
1354
|
+
* then, right before finalizeChanges pushes, checks again that the owner still
|
|
1355
|
+
* has access and that the run was not stopped meanwhile. Shared by a finished
|
|
1356
|
+
* run and one stopped at its budget; throws whatever stops the push.
|
|
1357
|
+
*/
|
|
1358
|
+
async pushChanges(run, handle, collectedFor, output, summary, existingWorkspace, options = {}) {
|
|
1359
|
+
const preparedInput = this.preflight(collectedFor, { launched: true });
|
|
1360
|
+
const workspace = existingWorkspace ?? (await this.options.vcs.recoverWorkspace(preparedInput));
|
|
1361
|
+
if (!workspace)
|
|
1362
|
+
throw new Error("coding_workspace_lost");
|
|
1363
|
+
await this.options.jobs.materializeWorkspace(handle, workspace.workspacePath);
|
|
1364
|
+
await normalizeCollectedLockfiles(collectedFor.provider, workspace.workspacePath);
|
|
1365
|
+
const current = await this.requireCurrent(run.runId);
|
|
1366
|
+
// One more (usually cached) check right before anything is pushed: access
|
|
1367
|
+
// revoked while the run was working stops the push.
|
|
1368
|
+
await this.authorizeRepository(current, { inFlight: true });
|
|
1369
|
+
// And a run stopped while its result was collected (stop_run, a cancelled task) pushes nothing.
|
|
1370
|
+
this.requireActive(await this.requireCurrent(run.runId));
|
|
1371
|
+
const report = await this.pullRequestRegistryReport(current);
|
|
1372
|
+
// keepDescription: a continuation's existing description is left as it is (no update, no proposal).
|
|
1373
|
+
const descriptionState = options.keepDescription ? null : await this.descriptionStateFor(current);
|
|
1374
|
+
const finalized = await this.options.vcs.finalizeChanges(workspace, {
|
|
1375
|
+
summary,
|
|
1376
|
+
tests: output.tests,
|
|
1377
|
+
tag: output.tag,
|
|
1378
|
+
...report,
|
|
1379
|
+
...restoredDependenciesFor(output),
|
|
1380
|
+
...(await this.issueSourceFor(current)),
|
|
1381
|
+
...(await this.relatedFor(current)),
|
|
1382
|
+
...(descriptionState && "baseline" in descriptionState
|
|
1383
|
+
? { description: { baseline: descriptionState.baseline } }
|
|
1384
|
+
: {}),
|
|
1385
|
+
});
|
|
1386
|
+
return { current, workspace, finalized, descriptionState };
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* A run stopped at its budget whose worker exited cleanly keeps its changes so
|
|
1390
|
+
* far: they are pushed as the usual draft (or branch), with a summary that says
|
|
1391
|
+
* the work is unfinished. A fresh pull request's description starts with that
|
|
1392
|
+
* summary; a continuation leaves its pull request's description alone (the
|
|
1393
|
+
* status comment says the run stopped), and pushes nothing onto a pull request
|
|
1394
|
+
* that is no longer a draft. Null when there was nothing to push or the push
|
|
1395
|
+
* failed or was skipped for any reason: the run then ends out of budget without
|
|
1396
|
+
* them. Once pushed, the pushed outcome is always returned. Throws only when the
|
|
1397
|
+
* run was stopped meanwhile (the stop wrote its terminal row).
|
|
1398
|
+
*/
|
|
1399
|
+
async keepStoppedChanges(run, handle, current, output, existingWorkspace) {
|
|
1400
|
+
const summary = budgetStoppedSummary(output.summary, current.request);
|
|
1401
|
+
const continuesPullRequest = Boolean(current.rootCodingRunId) && hasPullRequestDescription(current);
|
|
1402
|
+
let pushed;
|
|
1403
|
+
try {
|
|
1404
|
+
let workspace = existingWorkspace;
|
|
1405
|
+
if (continuesPullRequest) {
|
|
1406
|
+
workspace ??= await this.options.vcs.recoverWorkspace(this.preflight(current, { launched: true }));
|
|
1407
|
+
if (!workspace)
|
|
1408
|
+
throw new Error("coding_workspace_lost");
|
|
1409
|
+
if (!(await this.continuationStillDraft(current, workspace))) {
|
|
1410
|
+
containerLog.info({ runId: run.runId }, "a run stopped at its budget pushes nothing onto a pull request that is no longer a draft");
|
|
1411
|
+
return null;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
pushed = await this.pushChanges(run, handle, current, output, summary, workspace, {
|
|
1415
|
+
keepDescription: continuesPullRequest,
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
catch (error) {
|
|
1419
|
+
if (error instanceof Error && error.message === "coding_run_no_longer_active")
|
|
1420
|
+
throw error;
|
|
1421
|
+
containerLog.warn({ runId: run.runId, reason: describeFailure(error) }, "could not push the changes of a run stopped at its budget; it ends without them");
|
|
1422
|
+
return null;
|
|
1423
|
+
}
|
|
1424
|
+
const { finalized } = pushed;
|
|
1425
|
+
if (finalized.outcome === "no_changes")
|
|
1426
|
+
return null;
|
|
1427
|
+
// Pushed: from here on the run records the pushed outcome whatever else fails.
|
|
1428
|
+
const description = await this.recordDescription(pushed.current, pushed.descriptionState, finalized.outcome === "pull_request_opened" || finalized.outcome === "pull_request_updated"
|
|
1429
|
+
? finalized.description
|
|
1430
|
+
: undefined).catch(() => ({}));
|
|
1431
|
+
let result;
|
|
1432
|
+
try {
|
|
1433
|
+
result = this.resultFor({ ...output, summary }, pushed.current, finalized.outcome, finalized);
|
|
1434
|
+
}
|
|
1435
|
+
catch (error) {
|
|
1436
|
+
containerLog.warn({ runId: run.runId, reason: describeFailure(error) }, "could not record the worker's summary and tests of a budget-stopped run; recording its push alone");
|
|
1437
|
+
result = this.resultFor({
|
|
1438
|
+
...output,
|
|
1439
|
+
summary: budgetStoppedSummary(WORKER_BUDGET_EXHAUSTED_SUMMARY, current.request),
|
|
1440
|
+
tests: [],
|
|
1441
|
+
tag: undefined,
|
|
1442
|
+
}, pushed.current, finalized.outcome, finalized);
|
|
1443
|
+
}
|
|
1444
|
+
return {
|
|
1445
|
+
current: pushed.current,
|
|
1446
|
+
finalized,
|
|
1447
|
+
result,
|
|
1448
|
+
record: { ...dependencyCacheRecord(output), ...pushedRecord(finalized), ...description },
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* The pull request description a continuation run is about to replace, for
|
|
1453
|
+
* its task. Undefined for a fresh run or when it cannot be loaded: the run
|
|
1454
|
+
* goes ahead without the note.
|
|
1455
|
+
*/
|
|
1456
|
+
async currentDescriptionText(run) {
|
|
1457
|
+
const store = this.options.store;
|
|
1458
|
+
if (!run.rootCodingRunId || !hasPullRequestDescription(run) || !store.pullRequestDescriptionBaseline) {
|
|
1459
|
+
return undefined;
|
|
1460
|
+
}
|
|
1461
|
+
try {
|
|
1462
|
+
return (await store.pullRequestDescriptionBaseline(run.rootCodingRunId)).previousAgentText;
|
|
1463
|
+
}
|
|
1464
|
+
catch (err) {
|
|
1465
|
+
containerLog.debug({ err, runId: run.runId }, "could not load the pull request description for the task");
|
|
1466
|
+
return undefined;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* The issue a continuation's pull request was opened for, read for its task
|
|
1471
|
+
* the way the first run had it: the root run's Jira issue, else the GitHub
|
|
1472
|
+
* issue whose mention started the work (same repository), else the issue the
|
|
1473
|
+
* description wardby last wrote closes (same repository). Each candidate is
|
|
1474
|
+
* read in turn until one is an issue. Undefined for a fresh or local run, or
|
|
1475
|
+
* on any failure: the run goes ahead without it.
|
|
1476
|
+
*/
|
|
1477
|
+
async linkedIssueFor(run, description) {
|
|
1478
|
+
const store = this.options.store;
|
|
1479
|
+
const read = this.options.readLinkedIssue;
|
|
1480
|
+
if (!run.rootCodingRunId || !hasPullRequestDescription(run) || !read || !store.linkedIssueSource)
|
|
1481
|
+
return undefined;
|
|
1482
|
+
let timer;
|
|
1483
|
+
const timedOut = new Promise((resolve) => {
|
|
1484
|
+
timer = setTimeout(() => {
|
|
1485
|
+
containerLog.warn({ runId: run.runId }, "reading the linked issue timed out; continuing without it");
|
|
1486
|
+
resolve(undefined);
|
|
1487
|
+
}, LINKED_ISSUE_READ_TIMEOUT_MS);
|
|
1488
|
+
});
|
|
1489
|
+
try {
|
|
1490
|
+
return await Promise.race([this.readLinkedIssue(run, run.rootCodingRunId, description, read), timedOut]);
|
|
1491
|
+
}
|
|
1492
|
+
catch (err) {
|
|
1493
|
+
containerLog.warn({ err, runId: run.runId }, "could not read the linked issue; continuing without it");
|
|
1494
|
+
return undefined;
|
|
1495
|
+
}
|
|
1496
|
+
finally {
|
|
1497
|
+
clearTimeout(timer);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
async readLinkedIssue(run, rootRunId, description, read) {
|
|
1501
|
+
const source = await this.options.store.linkedIssueSource(rootRunId);
|
|
1502
|
+
const candidates = [];
|
|
1503
|
+
if (source.issueProvider === "jira" && source.issueKey && ISSUE_KEY.test(source.issueKey)) {
|
|
1504
|
+
candidates.push({ provider: "jira", key: source.issueKey });
|
|
1505
|
+
}
|
|
1506
|
+
const status = source.hostStatus;
|
|
1507
|
+
if (status?.provider === "github" &&
|
|
1508
|
+
status.repository.toLowerCase() === run.repository.toLowerCase() &&
|
|
1509
|
+
Number.isSafeInteger(status.number) &&
|
|
1510
|
+
status.number > 0) {
|
|
1511
|
+
candidates.push({ provider: "github", repository: run.repository, number: status.number });
|
|
1512
|
+
}
|
|
1513
|
+
const closed = closingIssueNumber(description, run.repository);
|
|
1514
|
+
if (closed !== undefined && !candidates.some((ref) => ref.provider === "github" && ref.number === closed)) {
|
|
1515
|
+
candidates.push({ provider: "github", repository: run.repository, number: closed });
|
|
1516
|
+
}
|
|
1517
|
+
for (const ref of candidates) {
|
|
1518
|
+
let issue;
|
|
1519
|
+
try {
|
|
1520
|
+
issue = await read(ref);
|
|
1521
|
+
}
|
|
1522
|
+
catch (err) {
|
|
1523
|
+
// The next candidate may still be readable.
|
|
1524
|
+
containerLog.warn({ err, runId: run.runId, issue: ref }, "could not read a linked issue candidate");
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
if (!issue)
|
|
1528
|
+
continue;
|
|
1529
|
+
return ref.provider === "github"
|
|
1530
|
+
? { kind: "github", number: ref.number, title: issue.title, body: issue.body }
|
|
1531
|
+
: { kind: "jira", key: ref.key, title: issue.title, body: issue.body };
|
|
1532
|
+
}
|
|
1533
|
+
return undefined;
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* A fix round's description baseline (null: no description flow -- a fresh
|
|
1537
|
+
* run, or a store without it). Never throws: a load failure is recorded as
|
|
1538
|
+
* skipped:error and the push goes ahead without a description update.
|
|
1539
|
+
*/
|
|
1540
|
+
/**
|
|
1541
|
+
* Whether a continuation's pull request is still a draft: the host's answer
|
|
1542
|
+
* when it has one, else the root run's record of wardby marking it ready.
|
|
1543
|
+
* A failed record lookup counts as not a draft (nothing unfinished is pushed).
|
|
1544
|
+
*/
|
|
1545
|
+
async continuationStillDraft(run, workspace) {
|
|
1546
|
+
const draft = await this.options.vcs.continuationPullRequestDraft?.(workspace).catch(() => undefined);
|
|
1547
|
+
if (draft !== undefined)
|
|
1548
|
+
return draft;
|
|
1549
|
+
const store = this.options.store;
|
|
1550
|
+
if (!run.rootCodingRunId || !store.pullRequestMarkedReady)
|
|
1551
|
+
return true;
|
|
1552
|
+
try {
|
|
1553
|
+
return !(await store.pullRequestMarkedReady(run.rootCodingRunId));
|
|
1554
|
+
}
|
|
1555
|
+
catch (err) {
|
|
1556
|
+
containerLog.warn({ err, runId: run.runId }, "could not read whether the pull request was marked ready");
|
|
1557
|
+
return false;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
async descriptionStateFor(run) {
|
|
1561
|
+
const store = this.options.store;
|
|
1562
|
+
if (!run.rootCodingRunId || !hasPullRequestDescription(run) || !store.pullRequestDescriptionBaseline)
|
|
1563
|
+
return null;
|
|
1564
|
+
let stored;
|
|
1565
|
+
try {
|
|
1566
|
+
stored = await store.pullRequestDescriptionBaseline(run.rootCodingRunId);
|
|
1567
|
+
}
|
|
1568
|
+
catch (err) {
|
|
1569
|
+
containerLog.warn({ err, runId: run.runId }, "could not load the pull request description baseline");
|
|
1570
|
+
return { failed: true };
|
|
1571
|
+
}
|
|
1572
|
+
const { legacySource, ...baseline } = stored;
|
|
1573
|
+
if (baseline.recordedSha256 !== null || !legacySource)
|
|
1574
|
+
return { baseline };
|
|
1575
|
+
const legacySha256 = await this.legacyDescriptionSha256(run.rootCodingRunId, legacySource);
|
|
1576
|
+
return { baseline: legacySha256 ? { ...baseline, legacySha256 } : baseline };
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* A legacy pull request's description as its root run wrote it, rebuilt from
|
|
1580
|
+
* stored records (the final message, tests, packages, dependency cache line
|
|
1581
|
+
* and issue line; the Related section is not hashed), and hashed. Undefined
|
|
1582
|
+
* when the root stored no final message, or on any failure: the description
|
|
1583
|
+
* then counts as edited, the safe direction. A lock file line the root's
|
|
1584
|
+
* refusal warning may have carried is not stored, so such a description
|
|
1585
|
+
* never matches.
|
|
1586
|
+
*/
|
|
1587
|
+
async legacyDescriptionSha256(rootRunId, source) {
|
|
1588
|
+
if (!source.summary)
|
|
1589
|
+
return undefined;
|
|
1590
|
+
try {
|
|
1591
|
+
const tests = storedTests(source.tests);
|
|
1592
|
+
const restored = source.dependencyCache === undefined
|
|
1593
|
+
? []
|
|
1594
|
+
: restoredDependencyFolders(sanitizeDependencyCacheReport(source.dependencyCache));
|
|
1595
|
+
const body = pullRequestBody({
|
|
1596
|
+
repository: source.repository,
|
|
1597
|
+
baseRef: source.baseRef,
|
|
1598
|
+
headRef: source.headRef,
|
|
1599
|
+
runId: rootRunId,
|
|
1600
|
+
summary: source.summary,
|
|
1601
|
+
...(tests.length > 0 ? { tests } : {}),
|
|
1602
|
+
...(await this.loadRegistryReport(rootRunId)),
|
|
1603
|
+
...(restored.length > 0 ? { restoredDependencies: restored } : {}),
|
|
1604
|
+
...this.issueFor({ issueProvider: source.issueProvider ?? null, issueKey: source.issueKey ?? null }),
|
|
1605
|
+
});
|
|
1606
|
+
return descriptionSha256(body);
|
|
1607
|
+
}
|
|
1608
|
+
catch (err) {
|
|
1609
|
+
containerLog.debug({ err, rootRunId }, "could not rebuild a legacy pull request description");
|
|
1610
|
+
return undefined;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* The Packages section's input: on a continuation, every package installed
|
|
1615
|
+
* by the runs that pushed to the pull request plus this run (merged in run
|
|
1616
|
+
* order); refusals are this run's only, since they describe the current
|
|
1617
|
+
* state. A fresh run, or a failed listing, reports this run's alone.
|
|
1618
|
+
*/
|
|
1619
|
+
async pullRequestRegistryReport(run) {
|
|
1620
|
+
const own = await this.loadRegistryReport(run.runId);
|
|
1621
|
+
const store = this.options.store;
|
|
1622
|
+
if (!run.rootCodingRunId || !hasPullRequestDescription(run) || !store.pullRequestRunIds)
|
|
1623
|
+
return own;
|
|
1624
|
+
let earlier;
|
|
1625
|
+
try {
|
|
1626
|
+
earlier = (await store.pullRequestRunIds(run.rootCodingRunId)).filter((id) => id !== run.runId);
|
|
1627
|
+
}
|
|
1628
|
+
catch (err) {
|
|
1629
|
+
containerLog.warn({ err, runId: run.runId }, "could not list the pull request's runs; listing this run's packages");
|
|
1630
|
+
return own;
|
|
1631
|
+
}
|
|
1632
|
+
const reports = await Promise.all(earlier.map((id) => this.loadRegistryReport(id)));
|
|
1633
|
+
const packages = mergePackages([...reports.map((report) => report.packages ?? []), own.packages ?? []]);
|
|
1634
|
+
// Package audit mode is cumulative like packages: the banner stays on the
|
|
1635
|
+
// pull request once any of its runs had the mode.
|
|
1636
|
+
const packageAudit = mergePackageAuditReports([...reports.map((report) => report.packageAudit), own.packageAudit]);
|
|
1637
|
+
return {
|
|
1638
|
+
...(packages.length > 0 ? { packages } : {}),
|
|
1639
|
+
...(own.packageRefusals ? { packageRefusals: own.packageRefusals } : {}),
|
|
1640
|
+
...(packageAudit ? { packageAudit } : {}),
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
/**
|
|
1644
|
+
* The run's description columns for store.complete. A fresh run that wrote
|
|
1645
|
+
* its PR records the hash on its own (root) row; a fix round that wrote or
|
|
1646
|
+
* kept wardby's text compare-and-sets the root's hash. Never throws.
|
|
1647
|
+
*/
|
|
1648
|
+
async recordDescription(run, state, outcome) {
|
|
1649
|
+
if (!hasPullRequestDescription(run))
|
|
1650
|
+
return {};
|
|
1651
|
+
if (!run.rootCodingRunId) {
|
|
1652
|
+
return outcome?.kind === "written"
|
|
1653
|
+
? { prDescriptionOutcome: "written", prDescriptionSha256: outcome.sha256, prDescriptionRunId: run.runId }
|
|
1654
|
+
: {};
|
|
1655
|
+
}
|
|
1656
|
+
if (!state)
|
|
1657
|
+
return {};
|
|
1658
|
+
if ("failed" in state)
|
|
1659
|
+
return { prDescriptionOutcome: "skipped:error" };
|
|
1660
|
+
if (!outcome || outcome.kind === "written")
|
|
1661
|
+
return {};
|
|
1662
|
+
const label = descriptionOutcomeLabel(outcome);
|
|
1663
|
+
if (outcome.kind !== "replaced" && outcome.kind !== "refreshed" && outcome.kind !== "unchanged") {
|
|
1664
|
+
return { prDescriptionOutcome: label };
|
|
1665
|
+
}
|
|
1666
|
+
const store = this.options.store;
|
|
1667
|
+
if (!store.recordPullRequestDescription)
|
|
1668
|
+
return { prDescriptionOutcome: label };
|
|
1669
|
+
try {
|
|
1670
|
+
const won = await store.recordPullRequestDescription({
|
|
1671
|
+
rootRunId: run.rootCodingRunId,
|
|
1672
|
+
expectedSha256: state.baseline.recordedSha256,
|
|
1673
|
+
sha256: outcome.sha256,
|
|
1674
|
+
agentTextRunId: outcome.agentTextRunId,
|
|
1675
|
+
});
|
|
1676
|
+
// A lost race leaves the root's hash as the other write left it: the next
|
|
1677
|
+
// round sees a mismatch and falls back to a comment, the safe direction.
|
|
1678
|
+
if (!won) {
|
|
1679
|
+
containerLog.warn({ runId: run.runId, rootRunId: run.rootCodingRunId }, "pull request description hash changed meanwhile; not recorded");
|
|
1680
|
+
}
|
|
1681
|
+
return { prDescriptionOutcome: label };
|
|
1682
|
+
}
|
|
1683
|
+
catch (err) {
|
|
1684
|
+
containerLog.warn({ err, runId: run.runId }, "could not record the pull request description hash");
|
|
1685
|
+
return { prDescriptionOutcome: "skipped:error" };
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
/**
|
|
1689
|
+
* refreshDescriptionWithoutPush, capped at DESCRIPTION_REFRESH_TIMEOUT_MS so a
|
|
1690
|
+
* slow host cannot hold up store.complete; a timeout is skipped:error.
|
|
1691
|
+
*/
|
|
1692
|
+
async refreshDescriptionInTime(run, output, existingWorkspace) {
|
|
1693
|
+
let timer;
|
|
1694
|
+
const timedOut = new Promise((resolve) => {
|
|
1695
|
+
timer = setTimeout(() => {
|
|
1696
|
+
containerLog.warn({ runId: run.runId, timeoutMs: DESCRIPTION_REFRESH_TIMEOUT_MS }, "pull request description refresh timed out");
|
|
1697
|
+
resolve({ prDescriptionOutcome: "skipped:error" });
|
|
1698
|
+
}, DESCRIPTION_REFRESH_TIMEOUT_MS);
|
|
1699
|
+
});
|
|
1700
|
+
try {
|
|
1701
|
+
return await Promise.race([this.refreshDescriptionWithoutPush(run, output, existingWorkspace), timedOut]);
|
|
1702
|
+
}
|
|
1703
|
+
finally {
|
|
1704
|
+
clearTimeout(timer);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* A fix round that changed nothing still re-derives its PR description (no
|
|
1709
|
+
* push). Never throws: any failure is recorded as skipped:error.
|
|
1710
|
+
*/
|
|
1711
|
+
async refreshDescriptionWithoutPush(run, output, existingWorkspace) {
|
|
1712
|
+
const vcs = this.options.vcs;
|
|
1713
|
+
if (!run.rootCodingRunId || !hasPullRequestDescription(run) || !vcs.refreshPullRequestDescription)
|
|
1714
|
+
return {};
|
|
1715
|
+
const state = await this.descriptionStateFor(run);
|
|
1716
|
+
if (!state || "failed" in state)
|
|
1717
|
+
return this.recordDescription(run, state, undefined);
|
|
1718
|
+
try {
|
|
1719
|
+
const input = this.preflightForCleanup(run);
|
|
1720
|
+
// Only the pull request is needed: a run another process launched has no workspace here.
|
|
1721
|
+
const workspace = existingWorkspace ??
|
|
1722
|
+
(input ? await vcs.recoverWorkspace(input).catch(() => null) : null) ??
|
|
1723
|
+
this.continuationTargetOf(run);
|
|
1724
|
+
if (!workspace)
|
|
1725
|
+
throw new Error("coding_workspace_lost");
|
|
1726
|
+
// The same in-flight access check a push gets: a revoked owner's run edits nothing.
|
|
1727
|
+
await this.authorizeRepository(run, { inFlight: true });
|
|
1728
|
+
const details = {
|
|
1729
|
+
summary: output.summary,
|
|
1730
|
+
tests: output.tests,
|
|
1731
|
+
tag: output.tag,
|
|
1732
|
+
...(await this.pullRequestRegistryReport(run)),
|
|
1733
|
+
...restoredDependenciesFor(output),
|
|
1734
|
+
...(await this.issueSourceFor(run)),
|
|
1735
|
+
description: { baseline: state.baseline },
|
|
1736
|
+
};
|
|
1737
|
+
const outcome = await vcs.refreshPullRequestDescription(workspace, details);
|
|
1738
|
+
return await this.recordDescription(run, state, outcome);
|
|
1739
|
+
}
|
|
1740
|
+
catch (err) {
|
|
1741
|
+
containerLog.warn({ err, runId: run.runId }, "could not refresh the pull request description");
|
|
1742
|
+
return { prDescriptionOutcome: "skipped:error" };
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
/** Runs the onCodingRunTerminal hook; nothing it does, throws, or rejects reaches the caller. */
|
|
1746
|
+
async notifyTerminal(runId) {
|
|
1747
|
+
const hook = this.options.onCodingRunTerminal;
|
|
1748
|
+
if (!hook)
|
|
1749
|
+
return;
|
|
1750
|
+
try {
|
|
1751
|
+
await hook(runId);
|
|
1752
|
+
}
|
|
1753
|
+
catch (err) {
|
|
1754
|
+
containerLog.warn({ err, runId }, "coding run terminal hook failed");
|
|
923
1755
|
}
|
|
924
1756
|
}
|
|
925
1757
|
/** The package report is informational: any failure loading it (a
|
|
@@ -929,10 +1761,15 @@ export class ContainerExecutor {
|
|
|
929
1761
|
try {
|
|
930
1762
|
const report = await this.options.registryReport?.(runId);
|
|
931
1763
|
const packages = Array.isArray(report?.packages) ? report.packages : [];
|
|
932
|
-
|
|
1764
|
+
// A listing-only refusal is for get_run and the agent, not the PR (see LISTING_REFUSAL_FILENAME).
|
|
1765
|
+
const packageRefusals = Array.isArray(report?.packageRefusals) ? warningRefusals(report.packageRefusals) : [];
|
|
1766
|
+
const packageAudit = isPackageAuditMode(report?.packageAuditMode)
|
|
1767
|
+
? { mode: report.packageAuditMode, entries: Array.isArray(report.packageAudit) ? report.packageAudit : [] }
|
|
1768
|
+
: undefined;
|
|
933
1769
|
return {
|
|
934
1770
|
...(packages.length > 0 ? { packages } : {}),
|
|
935
1771
|
...(packageRefusals.length > 0 ? { packageRefusals } : {}),
|
|
1772
|
+
...(packageAudit ? { packageAudit } : {}),
|
|
936
1773
|
};
|
|
937
1774
|
}
|
|
938
1775
|
catch {
|
|
@@ -965,8 +1802,10 @@ export class ContainerExecutor {
|
|
|
965
1802
|
}
|
|
966
1803
|
resolveCodingWorkerImage(selector) {
|
|
967
1804
|
if (selector.provider === "claude-code") {
|
|
1805
|
+
// workerImageRef never replaces the Claude Code agent image (the container that holds the
|
|
1806
|
+
// run's key); on a Claude Code agent it selects the tool runner (claudeToolImage).
|
|
968
1807
|
this.claudeToolImage(selector);
|
|
969
|
-
const image =
|
|
1808
|
+
const image = this.options.claudeWorkerImage;
|
|
970
1809
|
if (!image)
|
|
971
1810
|
throw new Error("coding_provider_not_configured:claude-code");
|
|
972
1811
|
if (!isImmutableDockerImage(image))
|
|
@@ -976,7 +1815,7 @@ export class ContainerExecutor {
|
|
|
976
1815
|
if (selector.provider !== "codex") {
|
|
977
1816
|
throw new Error(`coding_provider_unsupported:${String(selector.provider)}`);
|
|
978
1817
|
}
|
|
979
|
-
if (selector.workerImageRef) {
|
|
1818
|
+
if (selector.workerImageRef !== null) {
|
|
980
1819
|
if (!isImmutableDockerImage(selector.workerImageRef))
|
|
981
1820
|
throw new Error("coding_worker_image_invalid");
|
|
982
1821
|
return selector.workerImageRef;
|
|
@@ -997,10 +1836,20 @@ export class ContainerExecutor {
|
|
|
997
1836
|
resolveCodingToolImage(selector) {
|
|
998
1837
|
return selector.provider === "claude-code" ? this.claudeToolImage(selector) : null;
|
|
999
1838
|
}
|
|
1000
|
-
/**
|
|
1839
|
+
/**
|
|
1840
|
+
* The tool runner is what runs a Claude agent's commands, so it is what the toolchain selects —
|
|
1841
|
+
* unless the agent has a BYO workerImageRef, which replaces the tool runner directly (never the
|
|
1842
|
+
* agent image; see resolveCodingWorkerImage).
|
|
1843
|
+
*/
|
|
1001
1844
|
claudeToolImage(selector) {
|
|
1002
1845
|
if (!this.options.claudeToolRunnerImage)
|
|
1003
1846
|
throw new Error("coding_provider_not_configured:claude-code");
|
|
1847
|
+
// An admin-set custom image (workerImageRef) is where this agent's commands run.
|
|
1848
|
+
if (selector.workerImageRef !== null) {
|
|
1849
|
+
if (!isImmutableDockerImage(selector.workerImageRef))
|
|
1850
|
+
throw new Error("coding_worker_image_invalid");
|
|
1851
|
+
return selector.workerImageRef;
|
|
1852
|
+
}
|
|
1004
1853
|
const image = selector.toolchain === "node" && selector.toolchainVersion === null
|
|
1005
1854
|
? this.options.claudeToolRunnerImage
|
|
1006
1855
|
: selector.toolchainVersion !== null
|
|
@@ -1038,12 +1887,93 @@ export class ContainerExecutor {
|
|
|
1038
1887
|
supportsCodingServices(provider) {
|
|
1039
1888
|
return this.options.jobs.supportsServicesFor?.(provider) === true;
|
|
1040
1889
|
}
|
|
1890
|
+
/** Delegates to the job launcher's own warm-up (Executor.warmUp); a no-op for a launcher without one. */
|
|
1891
|
+
async warmUp() {
|
|
1892
|
+
await this.options.jobs.warmUp?.();
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Throws coding_run_no_longer_active once something else made the run terminal (a stop's
|
|
1896
|
+
* cancellation fence): nothing may then be pushed, opened, or rewritten on the host for it.
|
|
1897
|
+
*/
|
|
1898
|
+
requireActive(run) {
|
|
1899
|
+
if (TERMINAL_STATUSES.has(run.status))
|
|
1900
|
+
throw new Error("coding_run_no_longer_active");
|
|
1901
|
+
return run;
|
|
1902
|
+
}
|
|
1041
1903
|
async requireCurrent(runId) {
|
|
1042
1904
|
const current = await this.options.store.load(runId);
|
|
1043
1905
|
if (!current)
|
|
1044
1906
|
throw new Error("coding_run_not_found");
|
|
1045
1907
|
return current;
|
|
1046
1908
|
}
|
|
1909
|
+
/**
|
|
1910
|
+
* Recovery's half of a capacity refusal: the launcher recorded it, but the process died before the
|
|
1911
|
+
* store put the run back in the queue. The planned handle the attempt persisted is the ownership
|
|
1912
|
+
* proof; the run waits in the queue again, never reported ended.
|
|
1913
|
+
*/
|
|
1914
|
+
async finishCapacityRequeue(run, planned) {
|
|
1915
|
+
const outcome = await this.options.store.requeueAfterCapacityRefusal(run.runId, { planned });
|
|
1916
|
+
if (outcome === "spend_recorded") {
|
|
1917
|
+
await this.abandon(run, "coding_ambiguous_provisioning");
|
|
1918
|
+
return { state: "lost", reason: "coding_ambiguous_provisioning" };
|
|
1919
|
+
}
|
|
1920
|
+
if (outcome === "not_owned") {
|
|
1921
|
+
const current = await this.options.store.load(run.runId);
|
|
1922
|
+
if (!current || !TERMINAL_STATUSES.has(current.status))
|
|
1923
|
+
return { state: "active" };
|
|
1924
|
+
await this.cleanupTerminal(current);
|
|
1925
|
+
return { state: "terminal" };
|
|
1926
|
+
}
|
|
1927
|
+
// The session went with the requeue; the rest of what the dead attempt left is removed here.
|
|
1928
|
+
this.options.capabilities.delete(run.runId);
|
|
1929
|
+
const input = this.preflightForCleanup(run);
|
|
1930
|
+
const workspace = input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null;
|
|
1931
|
+
if (workspace)
|
|
1932
|
+
await this.options.vcs.cleanup(workspace).catch(() => undefined);
|
|
1933
|
+
await rm(this.artifactPath(run.runId), { recursive: true, force: true }).catch(() => undefined);
|
|
1934
|
+
this.emit({ stage: "queued", runId: run.runId });
|
|
1935
|
+
containerLog.info({ runId: run.runId }, "coding run waiting for capacity: recovered a refusal its process left");
|
|
1936
|
+
return { state: "active" };
|
|
1937
|
+
}
|
|
1938
|
+
/**
|
|
1939
|
+
* After drainCodingQueue failed a queued run (coding_queue_timeout): releases what an earlier
|
|
1940
|
+
* attempt the cluster refused left behind, as stop() does for a queued run. A refused
|
|
1941
|
+
* continuation already marked its pull request as being worked on; that check is finished
|
|
1942
|
+
* here. Best effort; the artifact is removed only where this process wrote it.
|
|
1943
|
+
*/
|
|
1944
|
+
async cleanupQueueTimeout(runId) {
|
|
1945
|
+
this.capacityRefusedAt.delete(runId);
|
|
1946
|
+
this.options.capabilities.delete(runId);
|
|
1947
|
+
const run = await this.options.store.load(runId);
|
|
1948
|
+
if (run) {
|
|
1949
|
+
const input = this.preflightForCleanup(run);
|
|
1950
|
+
const workspace = input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null;
|
|
1951
|
+
if (workspace)
|
|
1952
|
+
await this.options.vcs.cleanup(workspace).catch(() => undefined);
|
|
1953
|
+
const target = workspace ?? this.continuationTargetOf(run);
|
|
1954
|
+
if (target)
|
|
1955
|
+
await this.options.vcs.notifyContinuationFinished?.(target, "failed", { agentName: run.agentName });
|
|
1956
|
+
}
|
|
1957
|
+
await rm(this.artifactPath(runId), { recursive: true, force: true }).catch(() => undefined);
|
|
1958
|
+
}
|
|
1959
|
+
recordCapacityRefusal(runId) {
|
|
1960
|
+
const now = this.now().getTime();
|
|
1961
|
+
// Drop the ones whose backoff is over, so runs that never come back (timed out in the queue) do not pile up.
|
|
1962
|
+
for (const [id, at] of this.capacityRefusedAt) {
|
|
1963
|
+
if (now - at >= CAPACITY_RETRY_BACKOFF_MS)
|
|
1964
|
+
this.capacityRefusedAt.delete(id);
|
|
1965
|
+
}
|
|
1966
|
+
this.capacityRefusedAt.set(runId, now);
|
|
1967
|
+
}
|
|
1968
|
+
inCapacityBackoff(runId) {
|
|
1969
|
+
const at = this.capacityRefusedAt.get(runId);
|
|
1970
|
+
if (at === undefined)
|
|
1971
|
+
return false;
|
|
1972
|
+
if (this.now().getTime() - at < CAPACITY_RETRY_BACKOFF_MS)
|
|
1973
|
+
return true;
|
|
1974
|
+
this.capacityRefusedAt.delete(runId);
|
|
1975
|
+
return false;
|
|
1976
|
+
}
|
|
1047
1977
|
/**
|
|
1048
1978
|
* False when the launcher reports this run's job would not fit the cluster right now; the run
|
|
1049
1979
|
* is then queued and drainCodingQueue retries it. Launchers and stores without the hooks, and
|
|
@@ -1085,6 +2015,10 @@ export class ContainerExecutor {
|
|
|
1085
2015
|
// A run keeps the tool image it was dispatched with; rows from before CodingRun.toolImage use the default.
|
|
1086
2016
|
...(provider === "claude-code" ? { toolImage: run.toolImage ?? this.options.claudeToolRunnerImage } : {}),
|
|
1087
2017
|
inputArtifact,
|
|
2018
|
+
// Only when on: every other run's spec stays exactly what it was.
|
|
2019
|
+
...(run.dependencyCache ? { dependencyCache: true } : {}),
|
|
2020
|
+
// Claude Code only: the launcher gives the SDK's config dir, where the session grows, its own scratch.
|
|
2021
|
+
...(run.sessionResume && provider === "claude-code" ? { sessionResume: true } : {}),
|
|
1088
2022
|
timeoutSec: run.timeoutSec,
|
|
1089
2023
|
limits: { ...this.options.limits, ...(run.workspaceDiskMb ? { diskMb: run.workspaceDiskMb } : {}) },
|
|
1090
2024
|
labels: {},
|
|
@@ -1099,7 +2033,7 @@ export class ContainerExecutor {
|
|
|
1099
2033
|
return this.options.anthropicCredentialRef;
|
|
1100
2034
|
throw new Error("coding_provider_not_configured:claude-code");
|
|
1101
2035
|
}
|
|
1102
|
-
async writeInput(run, deadlineAt,
|
|
2036
|
+
async writeInput(run, deadlineAt, workspace) {
|
|
1103
2037
|
await ensurePrivateDirectory(this.artifactRoot);
|
|
1104
2038
|
const rootReal = await realpath(this.artifactRoot);
|
|
1105
2039
|
const directory = resolve(rootReal, run.runId);
|
|
@@ -1111,11 +2045,29 @@ export class ContainerExecutor {
|
|
|
1111
2045
|
const destination = join(directory, "input.json");
|
|
1112
2046
|
const temporary = join(directory, `.input-${randomUUID()}.tmp`);
|
|
1113
2047
|
const services = parseStoredServices(run.services);
|
|
1114
|
-
const
|
|
1115
|
-
|
|
2048
|
+
const withCommit = withBaseCommit(run.task, workspace.baseCommit);
|
|
2049
|
+
const description = await this.currentDescriptionText(run);
|
|
2050
|
+
// The ticket first, then the description: each is cut to what's left of the task's budget.
|
|
2051
|
+
// The target-branch merge note (conflicts to resolve) comes before the ticket and the description.
|
|
2052
|
+
const withMerge = withTargetMerge(withCommit, workspace.targetMerge, run.headRef);
|
|
2053
|
+
const task = withPullRequestDescription(withLinkedIssue(withMerge, await this.linkedIssueFor(run, description)), description);
|
|
2054
|
+
if (withCommit === run.task && /^[0-9a-f]{40}$/.test(workspace.baseCommit)) {
|
|
1116
2055
|
containerLog.warn({ event: "coding.base_commit_omitted", runId: run.runId }, "the coding task left no room for the base commit line");
|
|
1117
2056
|
}
|
|
1118
|
-
|
|
2057
|
+
// A repository context read must never fail a run ("the run never fails because of context"):
|
|
2058
|
+
// any fs error (an inaccessible workspace, EACCES, ...) is logged once and swallowed, leaving
|
|
2059
|
+
// claudeContext null, exactly as if the run had nothing to load.
|
|
2060
|
+
let claudeContext = null;
|
|
2061
|
+
if (run.provider === "claude-code") {
|
|
2062
|
+
try {
|
|
2063
|
+
claudeContext = await buildClaudeContext(workspace.workspacePath, { skills: run.repoSkills !== false });
|
|
2064
|
+
}
|
|
2065
|
+
catch (error) {
|
|
2066
|
+
const code = error?.code;
|
|
2067
|
+
containerLog.warn({ event: "coding.claude_context_unavailable", runId: run.runId, ...(code ? { code } : {}) }, "repository context could not be read; continuing without it");
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
const build = (contextFiles) => CodingTaskInputSchema.parse({
|
|
1119
2071
|
schemaVersion: CODING_PROTOCOL_VERSION,
|
|
1120
2072
|
runId: run.runId,
|
|
1121
2073
|
repository: run.repository,
|
|
@@ -1123,24 +2075,124 @@ export class ContainerExecutor {
|
|
|
1123
2075
|
headRef: run.headRef,
|
|
1124
2076
|
task,
|
|
1125
2077
|
model: run.model,
|
|
1126
|
-
|
|
2078
|
+
// The run's limit, not its grant: the proxy enforces the grant, and top-ups raise it while
|
|
2079
|
+
// the job runs. Claude Code's own budget stop (its SDK maxBudgetUsd) is set from this, so it
|
|
2080
|
+
// never stops a run before a top-up can; Codex has no stop of its own.
|
|
2081
|
+
budgetUsd: run.budgetLimitUsd ?? run.budgetUsd,
|
|
1127
2082
|
deadlineAt: deadlineAt.toISOString(),
|
|
1128
2083
|
continuationOf: run.rootCodingRunId ? { runId: run.rootCodingRunId } : undefined,
|
|
1129
2084
|
// Only when true: an untraced run's input stays exactly what older workers expect.
|
|
1130
2085
|
...(run.debugTrace ? { debugTrace: true } : {}),
|
|
2086
|
+
// Only on a traced run whose agent sets a cap: every other run's input stays exactly what older workers expect.
|
|
2087
|
+
...(run.debugTrace && run.debugTraceMaxMb ? { debugTraceMaxMb: run.debugTraceMaxMb } : {}),
|
|
1131
2088
|
// Only when the agent sets one: every other run's input stays exactly what older workers expect.
|
|
1132
2089
|
...(run.maxTurns ? { maxTurns: run.maxTurns } : {}),
|
|
2090
|
+
// Only when the agent sets one: every other run's input stays exactly what older workers expect.
|
|
2091
|
+
...(run.maxOutputTokens ? { maxOutputTokens: run.maxOutputTokens } : {}),
|
|
2092
|
+
// Only when off: every other run's input stays exactly what older workers expect.
|
|
2093
|
+
...(run.repoSkills === false ? { repoSkills: false } : {}),
|
|
2094
|
+
// Only when off: every other run's input stays exactly what older workers expect.
|
|
2095
|
+
...(run.claudeBareMode === false ? { claudeBareMode: false } : {}),
|
|
2096
|
+
// Only when on: every other run's input stays exactly what older workers expect. The volume
|
|
2097
|
+
// size (the job spec's limits.diskMb) bounds what the cache may restore.
|
|
2098
|
+
...(run.dependencyCache
|
|
2099
|
+
? { dependencyCache: true, diskMb: run.workspaceDiskMb ?? this.options.limits.diskMb }
|
|
2100
|
+
: {}),
|
|
2101
|
+
// Only when on: every other run's input stays exactly what older workers expect.
|
|
2102
|
+
...(run.sessionResume && run.provider === "claude-code" ? { sessionResume: true } : {}),
|
|
1133
2103
|
// Only when there are some: every other run's input stays exactly what older workers expect.
|
|
1134
2104
|
...(services.length > 0 ? { services: workerServices(services) } : {}),
|
|
2105
|
+
// Only when there is something to load: every other run's input stays exactly what older workers expect.
|
|
2106
|
+
...(contextFiles.length > 0 ? { claudeContext: { files: contextFiles } } : {}),
|
|
1135
2107
|
});
|
|
2108
|
+
// The content limits bound bytes, but JSON escaping can grow them up to sixfold (a control
|
|
2109
|
+
// character becomes \u00XX), so a hostile repository could still push input.json past
|
|
2110
|
+
// MAX_CODING_INPUT_BYTES and fail the launch. Drop context files from the tail (skills, then the
|
|
2111
|
+
// deepest imports; the root instructions go last) until the serialized input fits.
|
|
2112
|
+
const contextFiles = [...(claudeContext?.files ?? [])];
|
|
2113
|
+
const skipped = [...(claudeContext?.skipped ?? [])];
|
|
2114
|
+
let skippedOverflow = claudeContext?.skippedOverflow ?? 0;
|
|
2115
|
+
let serialized = JSON.stringify(build(contextFiles));
|
|
2116
|
+
while (contextFiles.length > 0 && Buffer.byteLength(serialized) > MAX_CODING_INPUT_BYTES) {
|
|
2117
|
+
const dropped = contextFiles.pop();
|
|
2118
|
+
if (skipped.length < MAX_RECORDED_SKIPS)
|
|
2119
|
+
skipped.push({ path: dropped.path, reason: "limit" });
|
|
2120
|
+
else
|
|
2121
|
+
skippedOverflow++;
|
|
2122
|
+
serialized = JSON.stringify(build(contextFiles));
|
|
2123
|
+
}
|
|
2124
|
+
for (const skip of skipped) {
|
|
2125
|
+
containerLog.warn({
|
|
2126
|
+
event: "coding.claude_context_skipped",
|
|
2127
|
+
runId: run.runId,
|
|
2128
|
+
// A refused import is logged as written, which a repository can make arbitrarily long.
|
|
2129
|
+
path: skip.path.slice(0, MAX_LOGGED_SKIP_PATH_CHARS),
|
|
2130
|
+
reason: skip.reason,
|
|
2131
|
+
}, "a repository context file was not loaded");
|
|
2132
|
+
}
|
|
2133
|
+
if (skippedOverflow > 0) {
|
|
2134
|
+
containerLog.warn({ event: "coding.claude_context_skipped", runId: run.runId, overflow: skippedOverflow }, "further repository context files were not loaded");
|
|
2135
|
+
}
|
|
1136
2136
|
// The run directory is 0700; world-readable mode only crosses Docker's UID boundary.
|
|
1137
|
-
await writeFile(temporary,
|
|
2137
|
+
await writeFile(temporary, serialized, { flag: "wx", mode: 0o444 });
|
|
1138
2138
|
await rename(temporary, destination);
|
|
1139
2139
|
return destination;
|
|
1140
2140
|
}
|
|
1141
2141
|
artifactPath(runId) {
|
|
1142
2142
|
return resolve(this.artifactRoot, runId);
|
|
1143
2143
|
}
|
|
2144
|
+
/**
|
|
2145
|
+
* A continuation's pull request from the run record alone, for the host-side
|
|
2146
|
+
* finish signals when the workspace is gone (another control-plane process
|
|
2147
|
+
* launched the run). Null for a fresh run: it has no pull request yet.
|
|
2148
|
+
*/
|
|
2149
|
+
continuationTargetOf(run) {
|
|
2150
|
+
if (!run.rootCodingRunId)
|
|
2151
|
+
return null;
|
|
2152
|
+
return {
|
|
2153
|
+
runId: run.runId,
|
|
2154
|
+
repository: run.repository,
|
|
2155
|
+
baseRef: run.baseRef,
|
|
2156
|
+
headRef: run.headRef,
|
|
2157
|
+
continuation: { rootRunId: run.rootCodingRunId },
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Session resume: never throws, never changes the run's outcome. The recorder redacts; only a status
|
|
2162
|
+
* code is logged. The run capability is already gone from the vault, so it is not passed (the recorder
|
|
2163
|
+
* removes its fixed token shape).
|
|
2164
|
+
*/
|
|
2165
|
+
async recordTranscript(run, handle) {
|
|
2166
|
+
const recorder = this.options.transcripts;
|
|
2167
|
+
if (!recorder)
|
|
2168
|
+
return;
|
|
2169
|
+
const jobs = this.options.jobs;
|
|
2170
|
+
let timer;
|
|
2171
|
+
try {
|
|
2172
|
+
const recording = Promise.resolve()
|
|
2173
|
+
.then(() => recorder.record({
|
|
2174
|
+
runId: run.runId,
|
|
2175
|
+
agentId: run.agentId,
|
|
2176
|
+
provider: run.provider,
|
|
2177
|
+
read: (maxBytes) => jobs.readTranscript ? jobs.readTranscript(handle, maxBytes) : Promise.resolve(undefined),
|
|
2178
|
+
}))
|
|
2179
|
+
.catch(() => "failed");
|
|
2180
|
+
const timeout = new Promise((resolve) => {
|
|
2181
|
+
timer = setTimeout(() => resolve("timeout"), TRANSCRIPT_RECORD_TIMEOUT_MS);
|
|
2182
|
+
timer.unref();
|
|
2183
|
+
});
|
|
2184
|
+
const result = await Promise.race([recording, timeout]);
|
|
2185
|
+
if (result !== "stored")
|
|
2186
|
+
containerLog.info({ runId: run.runId, transcript: result }, "run transcript not stored");
|
|
2187
|
+
}
|
|
2188
|
+
catch {
|
|
2189
|
+
// never changes the run's outcome
|
|
2190
|
+
}
|
|
2191
|
+
finally {
|
|
2192
|
+
if (timer)
|
|
2193
|
+
clearTimeout(timer);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
1144
2196
|
preflightForCleanup(run) {
|
|
1145
2197
|
if (!Array.isArray(run.protectedPaths) || !run.protectedPaths.every((path) => typeof path === "string"))
|
|
1146
2198
|
return null;
|
|
@@ -1165,6 +2217,7 @@ export class ContainerExecutor {
|
|
|
1165
2217
|
const failure = this.failure(reason);
|
|
1166
2218
|
await this.options.store.terminate(run.runId, "lost", failure.error, failure.audit);
|
|
1167
2219
|
this.terminal(run, "lost", failure.audit);
|
|
2220
|
+
await this.recordRefusal(run.runId, run.proxySessionId);
|
|
1168
2221
|
if (run.proxySessionId)
|
|
1169
2222
|
await this.options.sessions.cancelSession(run.proxySessionId).catch(() => undefined);
|
|
1170
2223
|
if (run.jobHandle) {
|
|
@@ -1174,14 +2227,17 @@ export class ContainerExecutor {
|
|
|
1174
2227
|
}
|
|
1175
2228
|
const input = this.preflightForCleanup(run);
|
|
1176
2229
|
const workspace = input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null;
|
|
1177
|
-
if (workspace)
|
|
2230
|
+
if (workspace)
|
|
1178
2231
|
await this.options.vcs.cleanup(workspace).catch(() => undefined);
|
|
1179
|
-
|
|
1180
|
-
|
|
2232
|
+
const target = workspace ?? this.continuationTargetOf(run);
|
|
2233
|
+
if (target)
|
|
2234
|
+
await this.options.vcs.notifyContinuationFinished?.(target, "failed", { agentName: run.agentName });
|
|
1181
2235
|
await rm(this.artifactPath(run.runId), { recursive: true, force: true }).catch(() => undefined);
|
|
1182
2236
|
this.emit({ stage: "cleanup", runId: run.runId, jobId: run.jobHandle?.id, cleanupSucceeded: true });
|
|
1183
2237
|
}
|
|
1184
2238
|
async cleanupTerminal(run) {
|
|
2239
|
+
// A run another path ended (or one recovered after a crash) still gets its refusal; the copy is idempotent.
|
|
2240
|
+
await this.recordRefusal(run.runId, run.proxySessionId);
|
|
1185
2241
|
if (run.proxySessionId)
|
|
1186
2242
|
await this.options.sessions.cancelSession(run.proxySessionId).catch(() => undefined);
|
|
1187
2243
|
if (run.jobHandle) {
|
|
@@ -1211,7 +2267,9 @@ export class ContainerExecutor {
|
|
|
1211
2267
|
const protectedPathFailed = run.status === "failed" && run.failureCategory === PROTECTED_PATH_CATEGORY;
|
|
1212
2268
|
await this.options.vcs.notifyContinuationFinished?.(workspace, outcome, {
|
|
1213
2269
|
agentName: run.agentName,
|
|
1214
|
-
...(outcome === "
|
|
2270
|
+
...(outcome === "succeeded" ? this.overrunDetails(run) : {}),
|
|
2271
|
+
...(outcome === "budget_exhausted" ? { budgetSentence: await this.budgetSentence(run) } : {}),
|
|
2272
|
+
...(outcome === "budget_exhausted" ? storedChangesPushed(run.result) : {}),
|
|
1215
2273
|
...(providerClass ? { providerSentence: providerSentence(providerClass) } : {}),
|
|
1216
2274
|
...(unreadyServices ? { serviceSentence: serviceUnreadySentence(unreadyServices) } : {}),
|
|
1217
2275
|
...(protectedPathFailed ? { protectedPathSentence: protectedPathSentence(null) } : {}),
|
|
@@ -1220,6 +2278,16 @@ export class ContainerExecutor {
|
|
|
1220
2278
|
await rm(this.artifactPath(run.runId), { recursive: true, force: true }).catch(() => undefined);
|
|
1221
2279
|
this.emit({ stage: "cleanup", runId: run.runId, jobId: run.jobHandle?.id, cleanupSucceeded: true });
|
|
1222
2280
|
}
|
|
2281
|
+
/**
|
|
2282
|
+
* Records the session's budget refusal on the run, at every ending: a run the proxy refused once may
|
|
2283
|
+
* still finish, fail for another reason, or be stopped. A session with no refusal adds nothing.
|
|
2284
|
+
* Best effort; never throws.
|
|
2285
|
+
*/
|
|
2286
|
+
async recordRefusal(runId, sessionId) {
|
|
2287
|
+
if (!sessionId)
|
|
2288
|
+
return;
|
|
2289
|
+
await this.options.store.recordBudgetRefusal?.(runId, sessionId).catch(() => undefined);
|
|
2290
|
+
}
|
|
1223
2291
|
/** Whether the proxy refused this run's session for budget. Never throws. */
|
|
1224
2292
|
async outOfBudget(sessionId) {
|
|
1225
2293
|
if (!sessionId || !this.options.sessions.budgetExhausted)
|
|
@@ -1247,11 +2315,23 @@ export class ContainerExecutor {
|
|
|
1247
2315
|
audit: { failureCategory: `provider_${provider.class}`, diagnosticId: logged.audit.diagnosticId },
|
|
1248
2316
|
};
|
|
1249
2317
|
}
|
|
2318
|
+
/** The overrun sentence for a finished run that spent past its base budget, as status details. */
|
|
2319
|
+
overrunDetails(run) {
|
|
2320
|
+
if (run.budgetBaseUsd === undefined)
|
|
2321
|
+
return {};
|
|
2322
|
+
const sentence = overrunSentence(run.costUsd, run.budgetBaseUsd, run.budgetLimitUsd ?? run.budgetUsd);
|
|
2323
|
+
return sentence ? { overrunSentence: sentence } : {};
|
|
2324
|
+
}
|
|
1250
2325
|
/** The host-safe sentence for a run that ran out of budget. */
|
|
1251
|
-
budgetSentence(run) {
|
|
2326
|
+
async budgetSentence(run) {
|
|
2327
|
+
const loaded = await this.options.store.budgetEvent?.(run.runId).catch(() => null);
|
|
2328
|
+
if (loaded)
|
|
2329
|
+
return hostBudgetSentence(loaded.event);
|
|
1252
2330
|
return budgetSentence({
|
|
1253
2331
|
runBudgetUsd: run.budgetUsd,
|
|
1254
2332
|
agentBudgetUsd: run.agentBudgetUsd ?? run.budgetUsd,
|
|
2333
|
+
baseBudgetUsd: run.budgetBaseUsd,
|
|
2334
|
+
limitUsd: run.budgetLimitUsd,
|
|
1255
2335
|
budgetGroupName: run.budgetGroupName,
|
|
1256
2336
|
});
|
|
1257
2337
|
}
|
|
@@ -1259,10 +2339,13 @@ export class ContainerExecutor {
|
|
|
1259
2339
|
* A failure the proxy's budget refusal caused. The failure itself is still
|
|
1260
2340
|
* logged under its diagnostic id, so the operator keeps the real reason.
|
|
1261
2341
|
*/
|
|
1262
|
-
budgetFailure(error, options = {}) {
|
|
2342
|
+
async budgetFailure(runId, error, options = {}) {
|
|
1263
2343
|
const logged = this.failure(error, options);
|
|
2344
|
+
const loaded = await this.options.store.budgetEvent?.(runId).catch(() => null);
|
|
1264
2345
|
return {
|
|
1265
|
-
error:
|
|
2346
|
+
error: loaded
|
|
2347
|
+
? `coding_budget_exhausted: ${ownerBudgetSentence(loaded.event, loaded.agentName)}`
|
|
2348
|
+
: "coding_budget_exhausted",
|
|
1266
2349
|
audit: { failureCategory: "budget", diagnosticId: logged.audit.diagnosticId },
|
|
1267
2350
|
};
|
|
1268
2351
|
}
|
|
@@ -1275,6 +2358,7 @@ export class ContainerExecutor {
|
|
|
1275
2358
|
}
|
|
1276
2359
|
}
|
|
1277
2360
|
terminal(run, outcome, audit) {
|
|
2361
|
+
this.capacityRefusedAt.delete(run.runId);
|
|
1278
2362
|
this.emit({
|
|
1279
2363
|
stage: "terminal",
|
|
1280
2364
|
runId: run.runId,
|
|
@@ -1359,6 +2443,10 @@ const CATEGORY_BY_PREFIX = [
|
|
|
1359
2443
|
// Checked before the generic "vcs_" fallback: the collected diff touched a
|
|
1360
2444
|
// path this agent may not edit, not a generic workspace/git failure.
|
|
1361
2445
|
["vcs_protected_path:", PROTECTED_PATH_CATEGORY],
|
|
2446
|
+
// A continuation's target-branch merge (git.ts): conflict markers left in a
|
|
2447
|
+
// conflicted file, or a target branch that could not be fetched or merged.
|
|
2448
|
+
[CONFLICT_MARKERS_ERROR_PREFIX, MERGE_CONFLICT_CATEGORY],
|
|
2449
|
+
[TARGET_MERGE_FAILED_ERROR, TARGET_MERGE_CATEGORY],
|
|
1362
2450
|
// A continuation whose pull request was merged or closed: nothing pushed (coding/continuation-wording.ts).
|
|
1363
2451
|
[CONTINUATION_CLOSED_ERROR, CONTINUATION_CLOSED_CATEGORY],
|
|
1364
2452
|
// A provider whose worker images this deployment doesn't configure (codex or claude-code): configuration.
|
|
@@ -1417,6 +2505,53 @@ function failureCategory(error) {
|
|
|
1417
2505
|
return "cancelled";
|
|
1418
2506
|
return "executor";
|
|
1419
2507
|
}
|
|
2508
|
+
/** A stored CodingRun result's summary (the agent-written description text), when it has one. */
|
|
2509
|
+
function storedSummary(result) {
|
|
2510
|
+
const summary = storedField(result, "summary");
|
|
2511
|
+
return typeof summary === "string" ? summary : undefined;
|
|
2512
|
+
}
|
|
2513
|
+
/** One field of a stored JSON object; undefined when it isn't an object. */
|
|
2514
|
+
function storedField(value, key) {
|
|
2515
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
2516
|
+
return undefined;
|
|
2517
|
+
return value[key];
|
|
2518
|
+
}
|
|
2519
|
+
/** Stored test results the PR body can list (command + outcome); anything else is dropped. */
|
|
2520
|
+
function storedTests(value) {
|
|
2521
|
+
if (!Array.isArray(value))
|
|
2522
|
+
return [];
|
|
2523
|
+
return value.flatMap((test) => {
|
|
2524
|
+
const command = storedField(test, "command");
|
|
2525
|
+
const outcome = storedField(test, "outcome");
|
|
2526
|
+
return typeof command === "string" && (outcome === "passed" || outcome === "failed" || outcome === "skipped")
|
|
2527
|
+
? [{ command, outcome }]
|
|
2528
|
+
: [];
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
/**
|
|
2532
|
+
* Packages from several runs' reports, in order: one entry per ecosystem +
|
|
2533
|
+
* name + version, first appearance first, accepted advisories merged.
|
|
2534
|
+
*/
|
|
2535
|
+
function mergePackages(lists) {
|
|
2536
|
+
const merged = new Map();
|
|
2537
|
+
for (const list of lists) {
|
|
2538
|
+
for (const pkg of list) {
|
|
2539
|
+
const key = `${pkg.ecosystem}\0${pkg.name}\0${pkg.version}`;
|
|
2540
|
+
const existing = merged.get(key);
|
|
2541
|
+
if (!existing) {
|
|
2542
|
+
merged.set(key, {
|
|
2543
|
+
...pkg,
|
|
2544
|
+
...(pkg.acceptedAdvisories ? { acceptedAdvisories: [...pkg.acceptedAdvisories] } : {}),
|
|
2545
|
+
});
|
|
2546
|
+
continue;
|
|
2547
|
+
}
|
|
2548
|
+
if (pkg.acceptedAdvisories?.length) {
|
|
2549
|
+
existing.acceptedAdvisories = [...new Set([...(existing.acceptedAdvisories ?? []), ...pkg.acceptedAdvisories])];
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
return [...merged.values()];
|
|
2554
|
+
}
|
|
1420
2555
|
function stableJson(value) {
|
|
1421
2556
|
if (Array.isArray(value))
|
|
1422
2557
|
return `[${value.map(stableJson).join(",")}]`;
|