@wardby/cli 0.6.0 → 0.8.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 +5 -5
- package/README.md +6 -1
- package/deploy/local/docker-compose.quickstart-coding.yml +19 -0
- package/dist/claude-coding-worker/driver.d.ts +33 -3
- package/dist/claude-coding-worker/driver.js +151 -21
- package/dist/claude-coding-worker/main.js +38 -6
- package/dist/claude-coding-worker/sdk.js +29 -2
- 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/claude-plugin/command.d.ts +61 -0
- package/dist/claude-plugin/command.js +376 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +3 -1
- package/dist/cli.js +71 -13
- package/dist/coding/code-host.d.ts +16 -0
- package/dist/coding/code-host.js +60 -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/mcp-token.d.ts +12 -0
- package/dist/coding/mcp-token.js +26 -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 +46 -126
- package/dist/coding/profile.js +45 -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 +264 -346
- package/dist/coding/protocol.js +237 -12
- package/dist/coding/redact.d.ts +3 -0
- package/dist/coding/redact.js +17 -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 +52 -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 +8 -3
- package/dist/coding-worker/artifact.js +3 -1
- package/dist/coding-worker/debug-trace.d.ts +9 -6
- package/dist/coding-worker/debug-trace.js +16 -19
- 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 +124 -39
- package/dist/coding-worker/errors.d.ts +1 -0
- package/dist/coding-worker/errors.js +32 -0
- package/dist/coding-worker/main.js +5 -1
- package/dist/coding-worker/mcp-config.d.ts +27 -0
- package/dist/coding-worker/mcp-config.js +32 -0
- package/dist/coding-worker/sdk.d.ts +36 -0
- package/dist/coding-worker/sdk.js +28 -1
- 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 +15 -0
- package/dist/config/dependency-cache.d.ts +23 -0
- package/dist/config/dependency-cache.js +32 -0
- package/dist/config/mcp-call-limit.d.ts +8 -0
- package/dist/config/mcp-call-limit.js +18 -0
- package/dist/config/mcp-servers.d.ts +8 -0
- package/dist/config/mcp-servers.js +12 -0
- package/dist/config/providers.d.ts +12 -3
- package/dist/config/providers.js +55 -5
- package/dist/config/test-simulators.d.ts +92 -0
- package/dist/config/test-simulators.js +333 -0
- package/dist/config/tool-timeout.d.ts +1 -0
- package/dist/config/tool-timeout.js +18 -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-mcp.d.ts +36 -0
- package/dist/core/coding-mcp.js +253 -0
- package/dist/core/coding-queue.d.ts +2 -1
- package/dist/core/coding-queue.js +7 -0
- package/dist/core/coding-repository.d.ts +46 -0
- package/dist/core/coding-repository.js +92 -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/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/delegation-slots.d.ts +27 -0
- package/dist/core/delegation-slots.js +44 -0
- package/dist/core/dispatch.d.ts +48 -2
- package/dist/core/dispatch.js +166 -66
- package/dist/core/engine-native.js +1 -1
- package/dist/core/error-code.d.ts +38 -0
- package/dist/core/error-code.js +169 -0
- package/dist/core/grants.d.ts +46 -8
- package/dist/core/grants.js +99 -8
- package/dist/core/host-events.d.ts +18 -2
- package/dist/core/host-events.js +291 -27
- package/dist/core/host-status.d.ts +85 -3
- package/dist/core/host-status.js +300 -22
- package/dist/core/issue-bridge.d.ts +50 -6
- package/dist/core/issue-bridge.js +124 -33
- package/dist/core/issue-events.d.ts +1 -1
- package/dist/core/issue-events.js +4 -10
- package/dist/core/issue-status.d.ts +1 -1
- package/dist/core/issue-status.js +58 -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/mcp-client/client.d.ts +47 -0
- package/dist/core/mcp-client/client.js +141 -0
- package/dist/core/mcp-client/jsonrpc.d.ts +15 -0
- package/dist/core/mcp-client/jsonrpc.js +54 -0
- package/dist/core/mcp-client/results.d.ts +13 -0
- package/dist/core/mcp-client/results.js +42 -0
- package/dist/core/mcp-servers.d.ts +90 -0
- package/dist/core/mcp-servers.js +230 -0
- package/dist/core/mcp-tools.d.ts +108 -0
- package/dist/core/mcp-tools.js +287 -0
- package/dist/core/memory-tools.js +7 -0
- package/dist/core/merge-order-check.d.ts +2 -1
- package/dist/core/merge-order-check.js +44 -18
- package/dist/core/notification-dispatcher.js +2 -1
- package/dist/core/notification-templates.d.ts +1 -7
- package/dist/core/notification-templates.js +36 -2
- package/dist/core/pull-request-state-sync.js +2 -2
- package/dist/core/reconciler.d.ts +4 -3
- package/dist/core/reconciler.js +8 -1
- package/dist/core/related-pull-requests.d.ts +11 -3
- package/dist/core/related-pull-requests.js +49 -20
- package/dist/core/reply-parts.d.ts +21 -0
- package/dist/core/reply-parts.js +73 -0
- package/dist/core/review-fix.d.ts +1 -1
- package/dist/core/review-fix.js +4 -3
- package/dist/core/review-host-checks.d.ts +1 -1
- package/dist/core/review-host-tools.d.ts +3 -0
- package/dist/core/review-host-tools.js +102 -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 +145 -49
- package/dist/core/runner.js +719 -216
- 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 +12 -74
- package/dist/core/secret-broker-config.js +1 -1
- package/dist/core/secret-guard.d.ts +49 -0
- package/dist/core/secret-guard.js +119 -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 +11 -1
- package/dist/core/secrets.js +27 -1
- package/dist/core/serializable-retry.d.ts +18 -0
- package/dist/core/serializable-retry.js +43 -0
- package/dist/core/stop-run.d.ts +46 -0
- package/dist/core/stop-run.js +61 -0
- package/dist/core/tool-admin.d.ts +5 -1
- package/dist/core/tool-admin.js +7 -2
- package/dist/core/tool-names.d.ts +13 -0
- package/dist/core/tool-names.js +20 -0
- package/dist/core/webhooks.d.ts +4 -0
- package/dist/core/webhooks.js +68 -49
- package/dist/core/workflow-events.d.ts +14 -2
- package/dist/core/workflow-events.js +4 -0
- package/dist/core/workflow-run-events.d.ts +5 -3
- package/dist/core/workflow-run-events.js +45 -9
- 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 +65 -3
- package/dist/generated/prisma/client.d.ts +65 -3
- package/dist/generated/prisma/commonInputTypes.d.ts +266 -106
- package/dist/generated/prisma/enums.d.ts +10 -0
- package/dist/generated/prisma/enums.js +8 -0
- package/dist/generated/prisma/internal/class.d.ts +110 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +957 -9
- package/dist/generated/prisma/internal/prismaNamespace.js +166 -2
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +174 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +166 -2
- package/dist/generated/prisma/models/Agent.d.ts +377 -8
- package/dist/generated/prisma/models/AgentMcpServer.d.ts +1412 -0
- package/dist/generated/prisma/models/AgentMcpServer.js +1 -0
- package/dist/generated/prisma/models/AgentRepository.d.ts +81 -1
- package/dist/generated/prisma/models/AuthUser.d.ts +1 -1
- package/dist/generated/prisma/models/CodingAgentProfile.d.ts +228 -7
- package/dist/generated/prisma/models/CodingProxyRequest.d.ts +207 -3
- package/dist/generated/prisma/models/CodingProxySession.d.ts +232 -2
- package/dist/generated/prisma/models/CodingRun.d.ts +1712 -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/HostProvider.d.ts +2176 -0
- package/dist/generated/prisma/models/HostProvider.js +1 -0
- package/dist/generated/prisma/models/HostProviderCredential.d.ts +1285 -0
- package/dist/generated/prisma/models/HostProviderCredential.js +1 -0
- package/dist/generated/prisma/models/HostProviderOperation.d.ts +1470 -0
- package/dist/generated/prisma/models/HostProviderOperation.js +1 -0
- package/dist/generated/prisma/models/HostProviderTrustedAccount.d.ts +1145 -0
- package/dist/generated/prisma/models/HostProviderTrustedAccount.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/McpServer.d.ts +1500 -0
- package/dist/generated/prisma/models/McpServer.js +1 -0
- package/dist/generated/prisma/models/NativeGatewayCall.d.ts +77 -1
- package/dist/generated/prisma/models/NativeGatewaySession.d.ts +35 -1
- package/dist/generated/prisma/models/Principal.d.ts +167 -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/ResourceGrant.d.ts +1 -1
- package/dist/generated/prisma/models/Run.d.ts +738 -2
- package/dist/generated/prisma/models/RunBudgetEvent.d.ts +1211 -0
- package/dist/generated/prisma/models/RunBudgetEvent.js +1 -0
- 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 +387 -0
- package/dist/generated/prisma/models/Tool.d.ts +223 -1
- package/dist/generated/prisma/models.d.ts +10 -0
- package/dist/help/search.js +12 -3
- package/dist/help-index.json +2033 -110
- package/dist/import/create.js +48 -4
- package/dist/import/neutral-schema.d.ts +51 -281
- 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 +10 -2
- package/dist/mcp/auth/access.js +15 -1
- package/dist/mcp/auth/grants-cli.d.ts +6 -4
- package/dist/mcp/auth/grants-cli.js +42 -3
- package/dist/mcp/auth/ownership.d.ts +7 -7
- package/dist/mcp/auth/resource-server.d.ts +3 -1
- package/dist/mcp/auth/resource-server.js +17 -2
- 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 +3 -0
- package/dist/mcp/host-events/github-ingress.d.ts +2 -0
- package/dist/mcp/host-events/github-ingress.js +2 -1
- package/dist/mcp/index.d.ts +6 -0
- package/dist/mcp/index.js +69 -10
- 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 +295 -28
- package/dist/mcp/tools/datastore.js +17 -8
- package/dist/mcp/tools/grants.js +73 -29
- package/dist/mcp/tools/host-providers.d.ts +2 -0
- package/dist/mcp/tools/host-providers.js +215 -0
- package/dist/mcp/tools/mcp-servers.d.ts +26 -0
- package/dist/mcp/tools/mcp-servers.js +481 -0
- package/dist/mcp/tools/memory.js +11 -1
- package/dist/mcp/tools/notification-channels.js +2 -2
- package/dist/mcp/tools/packages.d.ts +2 -0
- package/dist/mcp/tools/packages.js +195 -0
- package/dist/mcp/tools/repositories.js +65 -12
- 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.js +209 -27
- package/dist/mcp/tools/scheduling.js +3 -1
- package/dist/mcp/tools/secret-elicitation-form.js +5 -0
- package/dist/mcp/tools/secret-elicitation-server.d.ts +6 -0
- package/dist/mcp/tools/secret-elicitation-server.js +10 -61
- package/dist/mcp/tools/secret-elicitation.d.ts +2 -0
- package/dist/mcp/tools/secret-elicitation.js +25 -2
- package/dist/mcp/tools/secrets.js +40 -6
- 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 +7 -5
- package/dist/mcp/tools/tools.js +64 -16
- package/dist/mcp/tools/trigger.js +28 -6
- package/dist/mcp/transport/http-limits.d.ts +2 -0
- package/dist/mcp/transport/http-limits.js +6 -2
- 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/mcp/webhooks/ingress.js +10 -0
- package/dist/native-worker/docker-launcher.d.ts +38 -2
- package/dist/native-worker/docker-launcher.js +65 -5
- package/dist/native-worker/gateway-server.d.ts +55 -3
- package/dist/native-worker/gateway-server.js +152 -34
- package/dist/native-worker/gateway.d.ts +29 -1
- package/dist/native-worker/gateway.js +74 -4
- package/dist/native-worker/http-server.d.ts +7 -1
- package/dist/native-worker/http-server.js +62 -7
- package/dist/native-worker/http-transport.js +1 -1
- package/dist/native-worker/kubernetes-launcher.d.ts +7 -3
- package/dist/native-worker/kubernetes-launcher.js +43 -3
- package/dist/native-worker/launch.d.ts +18 -1
- package/dist/native-worker/launch.js +6 -1
- package/dist/native-worker/ledger.d.ts +24 -4
- package/dist/native-worker/ledger.js +62 -8
- package/dist/native-worker/loopback.js +3 -8
- package/dist/native-worker/main.d.ts +2 -1
- package/dist/native-worker/main.js +14 -3
- package/dist/native-worker/protocol.d.ts +130 -577
- package/dist/native-worker/protocol.js +51 -2
- package/dist/native-worker/sandbox-executor.d.ts +17 -2
- package/dist/native-worker/sandbox-executor.js +78 -4
- 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 +1 -1
- package/dist/native-worker/stdio.js +6 -5
- package/dist/native-worker/warm-pool.d.ts +2 -2
- package/dist/native-worker/warm-pool.js +4 -4
- package/dist/native-worker/worker.js +28 -2
- package/dist/observability/metrics.js +6 -1
- package/dist/providers/auth/self-hosted.d.ts +1 -1
- package/dist/providers/coding-proxy/capability-hash.d.ts +2 -0
- package/dist/providers/coding-proxy/capability-hash.js +5 -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/mcp-route.d.ts +34 -0
- package/dist/providers/coding-proxy/mcp-route.js +166 -0
- package/dist/providers/coding-proxy/memory-ledger.d.ts +17 -2
- package/dist/providers/coding-proxy/memory-ledger.js +99 -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 +16 -2
- package/dist/providers/coding-proxy/prisma-ledger.js +209 -69
- package/dist/providers/coding-proxy/proxy.d.ts +21 -2
- package/dist/providers/coding-proxy/proxy.js +300 -48
- 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 +10 -0
- package/dist/providers/coding-proxy/runtime.js +109 -16
- package/dist/providers/coding-proxy/server.d.ts +9 -1
- package/dist/providers/coding-proxy/server.js +55 -1
- package/dist/providers/coding-proxy/types.d.ts +78 -1
- package/dist/providers/datastore/types.d.ts +5 -0
- package/dist/providers/engine/types.d.ts +17 -2
- package/dist/providers/engine/types.js +17 -0
- package/dist/providers/executor/composition.d.ts +10 -3
- package/dist/providers/executor/composition.js +52 -7
- package/dist/providers/executor/container.d.ts +394 -9
- package/dist/providers/executor/container.js +1210 -96
- 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 +50 -1
- package/dist/providers/executor/dbos.js +207 -20
- package/dist/providers/executor/deferred.d.ts +12 -24
- package/dist/providers/executor/deferred.js +10 -29
- 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 +2 -0
- package/dist/providers/executor/routing.js +4 -0
- package/dist/providers/executor/types.d.ts +13 -0
- package/dist/providers/host-tools/invoke.d.ts +37 -0
- package/dist/providers/host-tools/invoke.js +117 -0
- package/dist/providers/host-tools/operations.d.ts +65 -0
- package/dist/providers/host-tools/operations.js +149 -0
- package/dist/providers/host-tools/registry.d.ts +79 -0
- package/dist/providers/host-tools/registry.js +362 -0
- package/dist/providers/host-tools/request-guard.d.ts +17 -0
- package/dist/providers/host-tools/request-guard.js +99 -0
- package/dist/providers/host-tools/self-test.d.ts +64 -0
- package/dist/providers/host-tools/self-test.js +142 -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 +10 -2
- package/dist/providers/jobs/docker-isolation.js +47 -8
- package/dist/providers/jobs/docker.d.ts +9 -0
- package/dist/providers/jobs/docker.js +60 -11
- package/dist/providers/jobs/fake-kubernetes-api.d.ts +2 -0
- package/dist/providers/jobs/fake-kubernetes-api.js +3 -0
- package/dist/providers/jobs/kubernetes-api.d.ts +8 -0
- package/dist/providers/jobs/kubernetes-api.js +10 -0
- package/dist/providers/jobs/kubernetes-client.js +10 -1
- package/dist/providers/jobs/kubernetes-isolation.d.ts +5 -1
- package/dist/providers/jobs/kubernetes-isolation.js +32 -3
- package/dist/providers/jobs/kubernetes.d.ts +11 -1
- package/dist/providers/jobs/kubernetes.js +111 -17
- package/dist/providers/jobs/types.d.ts +39 -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 +29 -2
- package/dist/providers/review-host/github.js +224 -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 +140 -6
- package/dist/providers/review-host/types.js +9 -1
- 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 +39 -4
- package/dist/providers/vcs/git.js +339 -26
- 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 +183 -44
- package/dist/providers/vcs/github.js +455 -56
- package/dist/providers/vcs/index.d.ts +1 -1
- package/dist/providers/vcs/index.js +9 -2
- 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 +37 -1
- package/dist/providers/vcs/remote.js +7 -1
- package/dist/providers/vcs/routing.d.ts +16 -8
- package/dist/providers/vcs/routing.js +27 -6
- package/dist/providers/vcs/types.d.ts +150 -5
- package/dist/quickstart/coding-seed.d.ts +14 -3
- package/dist/quickstart/coding-seed.js +213 -4
- package/dist/quickstart/index.d.ts +5 -0
- package/dist/quickstart/index.js +21 -0
- 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/eval-core.d.ts +12 -2
- package/dist/sandbox/eval-core.js +20 -4
- 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 +5 -0
- package/dist/sandbox/host-functions.js +13 -13
- package/dist/sandbox/limits.d.ts +4 -1
- package/dist/sandbox/limits.js +4 -1
- package/dist/sandbox/run-in-sandbox.d.ts +6 -0
- package/dist/sandbox/run-in-sandbox.js +14 -2
- package/dist/sandbox/safe-fetch.d.ts +2 -0
- package/dist/sandbox/safe-fetch.js +1 -1
- package/dist/sandbox/secret-broker.d.ts +1 -0
- package/dist/sandbox/secret-broker.js +4 -5
- package/dist/sandbox/tool-capabilities.d.ts +10 -30
- package/dist/sandbox/tool-timeout.d.ts +32 -0
- package/dist/sandbox/tool-timeout.js +52 -0
- package/dist/sandbox/user-tool.d.ts +8 -1
- package/dist/sandbox/user-tool.js +9 -3
- package/dist/sandbox/zod-params.d.ts +37 -5
- package/dist/sandbox/zod-params.js +181 -26
- package/dist/serve.js +8 -1
- package/dist/viewer/api-schema.d.ts +349 -2627
- package/dist/viewer/api-schema.js +7 -0
- package/dist/viewer/build-schemas.js +6 -2
- package/dist/viewer/graph.js +2 -1
- package/dist/viewer/openapi.js +2 -4
- package/dist/wardby-bin.js +15 -0
- package/docs/README.md +11 -1
- package/docs/agent-recipes.md +95 -24
- package/docs/architecture-runtime.md +1 -1
- package/docs/claude-code-plugin.md +164 -0
- package/docs/code-review-agents.md +557 -41
- package/docs/coding-agent-setup.md +566 -8
- package/docs/coding-packages.md +1033 -90
- package/docs/coding-services.md +27 -12
- package/docs/coding-worker-byo-images.md +24 -2
- package/docs/coding-worker-isolation.md +183 -37
- package/docs/debugging-agent-runs.md +242 -0
- package/docs/getting-started-gke.md +129 -141
- package/docs/getting-started-identity-provider.md +53 -38
- package/docs/getting-started.md +17 -0
- package/docs/host-providers.md +294 -0
- package/docs/jira-agents.md +17 -8
- 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/mcp-servers.md +254 -0
- package/docs/native-sandbox.md +143 -31
- package/docs/observability.md +4 -0
- package/docs/security-deployment.md +139 -29
- package/docs/slack-notifications.md +35 -19
- package/docs/tool-secrets.md +45 -10
- package/docs/viewer-api.md +6 -0
- package/docs/viewer-api.openapi.yml +1 -1
- package/help/admin-viewer.md +28 -2
- package/help/agent-recipes.md +33 -2
- package/help/architecture-agent.md +11 -6
- package/help/brokered-secrets.md +3 -0
- package/help/build-claude-code-image.md +1 -1
- package/help/build-worker-image.md +3 -1
- package/help/builder-agent.md +19 -6
- package/help/claude-code-run-pane.md +40 -0
- package/help/code-review-agents.md +177 -6
- 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 +15 -8
- package/help/errors/budget-exhausted.md +187 -0
- package/help/errors/budget-group-exhausted.md +5 -1
- package/help/errors/coding-mcp-pins-unavailable.md +35 -0
- 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 +1 -1
- 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 +4 -0
- package/help/errors/graph-incomplete.md +43 -0
- package/help/errors/graph-limit.md +43 -0
- package/help/errors/host-provider-error.md +40 -0
- package/help/errors/host-tool-failed.md +36 -0
- package/help/errors/mcp-call-limit.md +22 -0
- package/help/errors/mcp-call-timeout.md +22 -0
- package/help/errors/mcp-server-auth-failed.md +22 -0
- package/help/errors/mcp-server-credentials-unavailable.md +45 -0
- package/help/errors/mcp-server-error.md +28 -0
- package/help/errors/mcp-server-unreachable.md +22 -0
- package/help/errors/mcp-servers-off.md +21 -0
- package/help/errors/mcp-tool-not-allowed.md +26 -0
- package/help/errors/mcp-tool-removed.md +22 -0
- package/help/errors/mcp-unavailable.md +25 -0
- package/help/errors/merge-conflict.md +49 -0
- package/help/errors/native-sandbox-capacity.md +1 -1
- package/help/errors/native-sandbox-deadline-exceeded.md +1 -1
- package/help/errors/native-sandbox-docker-failed.md +1 -1
- package/help/errors/native-sandbox-gateway-error.md +20 -0
- package/help/errors/native-sandbox-gateway-unavailable.md +1 -1
- package/help/errors/native-sandbox-image-not-pinned.md +1 -1
- package/help/errors/native-sandbox-isolation-mismatch.md +1 -1
- package/help/errors/native-sandbox-launch-abandoned.md +21 -0
- package/help/errors/native-sandbox-network-unenforced.md +1 -1
- package/help/errors/native-sandbox-requires-catalog.md +1 -1
- package/help/errors/native-sandbox-unavailable.md +1 -1
- package/help/errors/native-sandbox-warm-delivery-failed.md +1 -1
- package/help/errors/native-sandbox-worker-exited.md +4 -3
- package/help/errors/native-sandbox-worker-lost.md +1 -1
- package/help/errors/native-sandbox-worker-unready.md +1 -1
- 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/repository-not-listed.md +36 -0
- package/help/errors/repository-required.md +36 -0
- package/help/errors/run-lost.md +39 -0
- package/help/errors/run-transcript-download.md +44 -0
- package/help/errors/secret-in-value.md +69 -0
- package/help/errors/target-merge.md +40 -0
- package/help/errors/tool-timeout.md +38 -0
- package/help/fix-merge-conflicts.md +73 -0
- package/help/github-app-setup.md +132 -0
- package/help/github.md +22 -0
- package/help/host-providers.md +40 -0
- package/help/identity-and-access.md +6 -1
- package/help/jira.md +23 -6
- package/help/knowledge.md +16 -7
- package/help/local-repositories.md +1 -1
- package/help/low-power-mode.md +7 -1
- package/help/mcp-servers.md +73 -0
- package/help/mcp.md +7 -2
- package/help/merge-order-check.md +5 -0
- package/help/native-capabilities.md +55 -1
- package/help/native-sandbox.md +16 -2
- package/help/operating-agents.md +21 -0
- package/help/package-audit-mode.md +91 -0
- package/help/related-pull-requests.md +17 -10
- package/help/repo-instructions-and-skills.md +3 -1
- package/help/resume-runs-locally.md +134 -0
- package/help/review-fix-rounds.md +33 -1
- package/help/sharing-agents.md +59 -0
- package/help/slack-notifications.md +19 -5
- 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 +86 -7
- package/help/troubleshooting/debugging-agent-runs.md +29 -0
- package/package.json +18 -10
- 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/migrations/20261102000000_host_providers/migration.sql +56 -0
- package/prisma/migrations/20261104000000_coding_agent_repository_list/migration.sql +5 -0
- package/prisma/migrations/20261106000000_tool_timeout/migration.sql +4 -0
- package/prisma/migrations/20261107000000_mcp_servers/migration.sql +33 -0
- package/prisma/migrations/20261108000000_coding_mcp/migration.sql +5 -0
- package/prisma/migrations/20261109000000_task_run_index/migration.sql +3 -0
- package/prisma/schema.prisma +468 -109
- package/scripts/git-askpass.sh +1 -1
- package/dist/sandbox/generated/zod-to-json-schema.bundle.js +0 -5
|
@@ -1,7 +1,13 @@
|
|
|
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 { mcpTokenHash, mintMcpToken } from "../../coding/mcp-token.js";
|
|
7
|
+
import { isCodexCoding } from "../../core/mcp-tools.js";
|
|
8
|
+
import { codeProviderOf } from "../../coding/code-host.js";
|
|
9
|
+
import { withPullRequestDescription } from "../../coding/pr-description-note.js";
|
|
10
|
+
import { withLinkedIssue } from "../../coding/linked-issue-note.js";
|
|
5
11
|
import { buildClaudeContext, MAX_RECORDED_SKIPS } from "../../coding/claude-context.js";
|
|
6
12
|
import { normalizeCollectExclusions } from "../../coding/collect-exclude.js";
|
|
7
13
|
import { codingProviderForModelProvider } from "../../coding/provider.js";
|
|
@@ -10,31 +16,50 @@ import { parseStoredServices, storedServiceNames, workerServices } from "../../c
|
|
|
10
16
|
import { MAX_SERVICE_DECLARATION_BYTES, SERVICE_DECLARATION_PATH } from "../../coding/services/declaration.js";
|
|
11
17
|
import { SERVICE_UNREADY_CATEGORY, SERVICE_UNREADY_ERROR, serviceUnreadyName, serviceUnreadySentence, } from "../../coding/services/wording.js";
|
|
12
18
|
import { PROTECTED_PATH_CATEGORY, protectedPathFromError, protectedPathSentence, } from "../../coding/protected-path-wording.js";
|
|
13
|
-
import { CONTINUATION_CLOSED_CATEGORY, CONTINUATION_CLOSED_ERROR } from "../../coding/continuation-wording.js";
|
|
14
|
-
import {
|
|
19
|
+
import { budgetStoppedSummary, CONTINUATION_CLOSED_CATEGORY, CONTINUATION_CLOSED_ERROR, WORKER_BUDGET_EXHAUSTED_SUMMARY, } from "../../coding/continuation-wording.js";
|
|
20
|
+
import { CONFLICT_MARKERS_ERROR_PREFIX, MERGE_CONFLICT_CATEGORY, TARGET_MERGE_CATEGORY, TARGET_MERGE_FAILED_ERROR, conflictMarkersPathFromError, conflictMarkersSentence, mergeSentence, withTargetMerge, } from "../../coding/merge-wording.js";
|
|
21
|
+
import { budgetSentence, overrunSentence } from "../../core/budget-wording.js";
|
|
15
22
|
import { fileSelfDefect } from "../../core/self-defects.js";
|
|
16
23
|
import { isTransactionUnavailable } from "../../core/dispatch.js";
|
|
24
|
+
import { topUpCodingBudget } from "../../core/budget-topup.js";
|
|
17
25
|
import { CONTENDED_TX_MAX_WAIT_MS } from "../../core/timing.js";
|
|
18
26
|
import { classifyProviderFailure, providerClassOfCategory, providerSentence, } from "../../core/provider-wording.js";
|
|
19
27
|
import { logger } from "../../core/logger.js";
|
|
20
28
|
import { requiredLevel } from "../../core/repo-access.js";
|
|
21
29
|
import { ensurePrivateDirectory } from "../../core/private-directory.js";
|
|
22
30
|
import { codingRunObserver } from "../../coding/observability.js";
|
|
23
|
-
import { CODING_PROTOCOL_VERSION, CodingRunResultSchema, CodingTaskInputSchema, MAX_CODING_INPUT_BYTES,
|
|
31
|
+
import { CODING_PROTOCOL_VERSION, CodingRunResultSchema, CodingTaskInputSchema, MAX_CODING_INPUT_BYTES, isLocalRepository, parseCodingWorkerOutputJson, redactAndTruncate, restoredDependencyFolders, sanitizeDependencyCacheReport, } from "../../coding/protocol.js";
|
|
24
32
|
import { parseStoredEntry } from "../llm/catalog-types.js";
|
|
25
33
|
import { resolveCodingEntry } from "../../core/run-pricing.js";
|
|
26
34
|
import { CODING_PROXY_ALIAS, CODING_PROXY_PORT, isImmutableDockerImage } from "../jobs/docker-isolation.js";
|
|
27
35
|
import { normalizeRegistryLockfiles } from "../../coding/registry/lockfiles.js";
|
|
28
|
-
import {
|
|
36
|
+
import { warningRefusals } from "../../coding/registry/report.js";
|
|
37
|
+
import { hostBudgetSentence, loadBudgetEvent, ownerBudgetSentence, proxyRefusalDetails, recordBudgetEvent, } from "../../core/budget-events.js";
|
|
38
|
+
import { JobCapacityError, } from "../jobs/types.js";
|
|
39
|
+
import { ISSUE_KEY, issueReference } from "../issue-tracker/types.js";
|
|
29
40
|
import { HEARTBEAT_TIMEOUT_MS } from "../../core/timing.js";
|
|
30
41
|
import { collectRelatedPullRequests } from "../../core/related-pull-requests.js";
|
|
42
|
+
import { isIssueMentionTask } from "../../core/host-status.js";
|
|
43
|
+
import { pullRequestBody, } from "../vcs/github.js";
|
|
44
|
+
import { closingIssueNumber, descriptionSha256 } from "../vcs/pr-description.js";
|
|
31
45
|
const containerLog = logger.child({ module: "container-executor" });
|
|
46
|
+
/** A transcript read and store must not hold up the job's removal for long. */
|
|
47
|
+
const TRANSCRIPT_RECORD_TIMEOUT_MS = 60_000;
|
|
32
48
|
/** A logged context skip path is cut to this many characters: a refused import is reported as written. */
|
|
33
49
|
const MAX_LOGGED_SKIP_PATH_CHARS = 256;
|
|
34
50
|
/** The input path in a spec built only to ask the launcher about capacity; never written or read. */
|
|
35
51
|
const CAPACITY_PROBE_INPUT = "/dev/null";
|
|
36
52
|
const TERMINAL_STATUSES = new Set(["succeeded", "failed", "refused", "lost", "budget_exhausted", "cancelled"]);
|
|
37
53
|
const PROVISIONING_BACKEND = "provisioning";
|
|
54
|
+
/** The longest a no-change fix round's description refresh may hold up the run's terminal write. */
|
|
55
|
+
export const DESCRIPTION_REFRESH_TIMEOUT_MS = 60_000;
|
|
56
|
+
/**
|
|
57
|
+
* After the cluster refuses a run's launch for capacity, how long this executor leaves the run
|
|
58
|
+
* waiting in the queue before trying it again (drainCodingQueue starts it every tick meanwhile).
|
|
59
|
+
*/
|
|
60
|
+
export const CAPACITY_RETRY_BACKOFF_MS = 60_000;
|
|
61
|
+
/** How many runs above a root coding run are searched for the mention that started it. */
|
|
62
|
+
const MAX_LINKED_ISSUE_RUN_DEPTH = 8;
|
|
38
63
|
/** Serializes concurrency-slot claims across replicas. Distinct from the OAuth client lock (7412901). */
|
|
39
64
|
const CODING_SLOT_LOCK_SQL = "SELECT 1 AS locked FROM pg_advisory_xact_lock(7412902)";
|
|
40
65
|
/**
|
|
@@ -48,6 +73,14 @@ const CODING_SLOT_LOCK_SQL = "SELECT 1 AS locked FROM pg_advisory_xact_lock(7412
|
|
|
48
73
|
*/
|
|
49
74
|
class RunNoLongerActiveError extends Error {
|
|
50
75
|
}
|
|
76
|
+
/** Private sentinel: rolls back requeueAfterCapacityRefusal's transaction with the outcome it carries. */
|
|
77
|
+
class NotRequeuedError extends Error {
|
|
78
|
+
outcome;
|
|
79
|
+
constructor(outcome) {
|
|
80
|
+
super(outcome);
|
|
81
|
+
this.outcome = outcome;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
51
84
|
function proxyProtocol(provider) {
|
|
52
85
|
if (provider === "codex")
|
|
53
86
|
return "openai-responses";
|
|
@@ -68,6 +101,51 @@ export async function normalizeCollectedLockfiles(provider, workspacePath) {
|
|
|
68
101
|
proxyBaseUrl: `http://${CODING_PROXY_ALIAS}:${CODING_PROXY_PORT}`,
|
|
69
102
|
});
|
|
70
103
|
}
|
|
104
|
+
/** The worker's dependency cache report, as CodingRunRecord columns: nothing on a run without one. */
|
|
105
|
+
function dependencyCacheRecord(output) {
|
|
106
|
+
return output.dependencyCache ? { dependencyCacheResult: output.dependencyCache } : {};
|
|
107
|
+
}
|
|
108
|
+
/** What a push records on the run: its base commit, and a pushed branch's name (a local repository). */
|
|
109
|
+
function pushedRecord(finalized) {
|
|
110
|
+
return {
|
|
111
|
+
baseSha: finalized.baseCommit,
|
|
112
|
+
...(finalized.outcome === "branch_pushed" ? { resultBranch: finalized.headRef } : {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** The PR body's dependency cache line: the folders restored from a snapshot (hit or nearest). */
|
|
116
|
+
function restoredDependenciesFor(output) {
|
|
117
|
+
const folders = restoredDependencyFolders(output.dependencyCache);
|
|
118
|
+
return folders.length > 0 ? { restoredDependencies: folders } : {};
|
|
119
|
+
}
|
|
120
|
+
/** The stored form of a description outcome: written, replaced, refreshed:<reason>, unchanged, edited_by_hand, skipped:<reason>. */
|
|
121
|
+
export function descriptionOutcomeLabel(outcome) {
|
|
122
|
+
if (outcome.kind === "refreshed" || outcome.kind === "skipped")
|
|
123
|
+
return `${outcome.kind}:${outcome.reason}`;
|
|
124
|
+
return outcome.kind;
|
|
125
|
+
}
|
|
126
|
+
/** A local repository has no pull request: its runs get no description note and record no description outcome. */
|
|
127
|
+
function hasPullRequestDescription(run) {
|
|
128
|
+
return !isLocalRepository(run.repository);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* A budget-stopped run's stored result names what its kept changes became
|
|
132
|
+
* (see keepStoppedChanges): the `changesPushed` detail for its status comment,
|
|
133
|
+
* or nothing when it pushed nothing.
|
|
134
|
+
*/
|
|
135
|
+
function storedChangesPushed(result) {
|
|
136
|
+
const parsed = CodingRunResultSchema.safeParse(result);
|
|
137
|
+
if (!parsed.success)
|
|
138
|
+
return {};
|
|
139
|
+
const stored = parsed.data;
|
|
140
|
+
if (stored.outcome === "branch_pushed")
|
|
141
|
+
return { changesPushed: {} };
|
|
142
|
+
if (stored.outcome === "pull_request_opened" || stored.outcome === "pull_request_updated") {
|
|
143
|
+
return { changesPushed: { pullRequestNumber: stored.pullRequestNumber } };
|
|
144
|
+
}
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
/** The longest reading the linked issue may hold up a continuation's launch. */
|
|
148
|
+
export const LINKED_ISSUE_READ_TIMEOUT_MS = 15_000;
|
|
71
149
|
export class PrismaContainerExecutionStore {
|
|
72
150
|
db;
|
|
73
151
|
options;
|
|
@@ -94,13 +172,26 @@ export class PrismaContainerExecutionStore {
|
|
|
94
172
|
});
|
|
95
173
|
if (!row?.codingRun)
|
|
96
174
|
return null;
|
|
175
|
+
const link = row.codingRun.viaRepositoryLink
|
|
176
|
+
? await this.db.agentRepository.findFirst({
|
|
177
|
+
where: {
|
|
178
|
+
agentId: row.agentId,
|
|
179
|
+
repository: row.codingRun.repository,
|
|
180
|
+
access: "write",
|
|
181
|
+
triggers: { isEmpty: true },
|
|
182
|
+
},
|
|
183
|
+
select: { authorizedVia: true },
|
|
184
|
+
})
|
|
185
|
+
: null;
|
|
97
186
|
return {
|
|
98
187
|
runId: row.id,
|
|
188
|
+
agentId: row.agentId,
|
|
99
189
|
status: row.status,
|
|
100
190
|
agentKind: row.agent.kind,
|
|
101
191
|
agentName: row.agent.name,
|
|
102
192
|
ownerId: row.agent.ownerId,
|
|
103
193
|
task: row.codingRun.task,
|
|
194
|
+
...(row.codingRun.request ? { request: row.codingRun.request } : {}),
|
|
104
195
|
repository: row.codingRun.repository,
|
|
105
196
|
baseRef: row.codingRun.baseRef,
|
|
106
197
|
headRef: row.codingRun.headRef,
|
|
@@ -115,6 +206,8 @@ export class PrismaContainerExecutionStore {
|
|
|
115
206
|
mergeOrder: row.codingRun.mergeOrder,
|
|
116
207
|
budgetUsd: Number(row.codingRun.budgetReservedUsd),
|
|
117
208
|
agentBudgetUsd: Number(row.agent.budgetUsd),
|
|
209
|
+
...(row.codingRun.budgetBaseUsd == null ? {} : { budgetBaseUsd: Number(row.codingRun.budgetBaseUsd) }),
|
|
210
|
+
...(row.codingRun.budgetLimitUsd == null ? {} : { budgetLimitUsd: Number(row.codingRun.budgetLimitUsd) }),
|
|
118
211
|
budgetGroupName: row.agent.budgetGroup?.name ?? null,
|
|
119
212
|
tokensIn: row.tokensIn,
|
|
120
213
|
tokensOut: row.tokensOut,
|
|
@@ -129,13 +222,20 @@ export class PrismaContainerExecutionStore {
|
|
|
129
222
|
toolImage: row.codingRun.toolImage,
|
|
130
223
|
workspaceDiskMb: row.codingRun.workspaceDiskMb,
|
|
131
224
|
debugTrace: row.codingRun.debugTrace,
|
|
225
|
+
debugTraceMaxMb: row.codingRun.debugTraceMaxMb,
|
|
132
226
|
maxTurns: row.codingRun.maxTurns,
|
|
227
|
+
maxOutputTokens: row.codingRun.maxOutputTokens,
|
|
133
228
|
repoSkills: row.codingRun.repoSkills,
|
|
134
229
|
claudeBareMode: row.codingRun.claudeBareMode,
|
|
230
|
+
dependencyCache: row.codingRun.dependencyCache,
|
|
231
|
+
sessionResume: row.codingRun.sessionResume,
|
|
135
232
|
services: row.codingRun.services,
|
|
136
233
|
failureCategory: row.codingRun.failureCategory,
|
|
137
234
|
profileRepository: row.agent.codingProfile?.repository ?? null,
|
|
138
235
|
repositoryAuthorizedVia: row.agent.codingProfile?.repositoryAuthorizedVia ?? null,
|
|
236
|
+
viaRepositoryLink: row.codingRun.viaRepositoryLink,
|
|
237
|
+
repositoryLinkExists: link !== null,
|
|
238
|
+
repositoryLinkAuthorizedVia: link?.authorizedVia ?? null,
|
|
139
239
|
pricingVersion: row.pricingVersion,
|
|
140
240
|
pricingSnapshot: row.pricingSnapshot,
|
|
141
241
|
};
|
|
@@ -150,6 +250,108 @@ export class PrismaContainerExecutionStore {
|
|
|
150
250
|
...(pr.mergeOrder !== undefined ? { mergeOrder: pr.mergeOrder } : {}),
|
|
151
251
|
}));
|
|
152
252
|
}
|
|
253
|
+
async pullRequestMarkedReady(rootRunId) {
|
|
254
|
+
const root = await this.db.codingRun.findUnique({ where: { runId: rootRunId }, select: { markedReadyAt: true } });
|
|
255
|
+
return Boolean(root?.markedReadyAt);
|
|
256
|
+
}
|
|
257
|
+
async pullRequestDescriptionBaseline(rootRunId) {
|
|
258
|
+
const root = await this.db.codingRun.findUnique({
|
|
259
|
+
where: { runId: rootRunId },
|
|
260
|
+
select: {
|
|
261
|
+
prDescriptionSha256: true,
|
|
262
|
+
prDescriptionRunId: true,
|
|
263
|
+
result: true,
|
|
264
|
+
repository: true,
|
|
265
|
+
baseRef: true,
|
|
266
|
+
headRef: true,
|
|
267
|
+
dependencyCacheResult: true,
|
|
268
|
+
issueProvider: true,
|
|
269
|
+
issueKey: true,
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
if (!root)
|
|
273
|
+
throw new Error("coding_run_not_found");
|
|
274
|
+
// Legacy pull requests (no recorded author run) carry the root run's own text.
|
|
275
|
+
const agentTextRunId = root.prDescriptionRunId ?? rootRunId;
|
|
276
|
+
const source = agentTextRunId === rootRunId
|
|
277
|
+
? root.result
|
|
278
|
+
: (await this.db.codingRun.findUnique({ where: { runId: agentTextRunId }, select: { result: true } }))?.result;
|
|
279
|
+
const previousAgentText = storedSummary(source);
|
|
280
|
+
const rootSummary = storedSummary(root.result);
|
|
281
|
+
const legacySource = root.prDescriptionSha256 === null
|
|
282
|
+
? {
|
|
283
|
+
repository: root.repository,
|
|
284
|
+
baseRef: root.baseRef,
|
|
285
|
+
headRef: root.headRef,
|
|
286
|
+
...(rootSummary !== undefined ? { summary: rootSummary } : {}),
|
|
287
|
+
...(storedField(root.result, "tests") !== undefined ? { tests: storedField(root.result, "tests") } : {}),
|
|
288
|
+
...(root.dependencyCacheResult != null ? { dependencyCache: root.dependencyCacheResult } : {}),
|
|
289
|
+
issueProvider: root.issueProvider,
|
|
290
|
+
issueKey: root.issueKey,
|
|
291
|
+
}
|
|
292
|
+
: undefined;
|
|
293
|
+
return {
|
|
294
|
+
recordedSha256: root.prDescriptionSha256,
|
|
295
|
+
...(previousAgentText !== undefined ? { previousAgentText } : {}),
|
|
296
|
+
agentTextRunId,
|
|
297
|
+
...(legacySource ? { legacySource } : {}),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
async linkedIssueSource(rootRunId) {
|
|
301
|
+
const root = await this.db.codingRun.findUnique({
|
|
302
|
+
where: { runId: rootRunId },
|
|
303
|
+
select: { issueProvider: true, issueKey: true, run: { select: { parentRunId: true } } },
|
|
304
|
+
});
|
|
305
|
+
if (!root)
|
|
306
|
+
throw new Error("coding_run_not_found");
|
|
307
|
+
// The mention that started the work is on the root's delegating run, or a run above it.
|
|
308
|
+
let hostStatus = null;
|
|
309
|
+
let parentRunId = root.run.parentRunId;
|
|
310
|
+
for (let depth = 0; parentRunId && depth < MAX_LINKED_ISSUE_RUN_DEPTH; depth++) {
|
|
311
|
+
const parent = await this.db.run.findUnique({
|
|
312
|
+
where: { id: parentRunId },
|
|
313
|
+
select: {
|
|
314
|
+
parentRunId: true,
|
|
315
|
+
taskOverride: true,
|
|
316
|
+
hostStatus: { select: { provider: true, repository: true, number: true } },
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
if (!parent)
|
|
320
|
+
break;
|
|
321
|
+
if (parent.hostStatus) {
|
|
322
|
+
const { provider, repository, number } = parent.hostStatus;
|
|
323
|
+
hostStatus = {
|
|
324
|
+
provider,
|
|
325
|
+
repository,
|
|
326
|
+
number,
|
|
327
|
+
issueMention: provider === "github" && isIssueMentionTask(parent.taskOverride, repository, number),
|
|
328
|
+
};
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
parentRunId = parent.parentRunId;
|
|
332
|
+
}
|
|
333
|
+
return { issueProvider: root.issueProvider, issueKey: root.issueKey, hostStatus };
|
|
334
|
+
}
|
|
335
|
+
async pullRequestRunIds(rootRunId) {
|
|
336
|
+
const rows = await this.db.codingRun.findMany({
|
|
337
|
+
where: { OR: [{ runId: rootRunId }, { rootCodingRunId: rootRunId }] },
|
|
338
|
+
select: { runId: true, result: true },
|
|
339
|
+
orderBy: { run: { startedAt: "asc" } },
|
|
340
|
+
});
|
|
341
|
+
const pushed = rows.filter((row) => {
|
|
342
|
+
const outcome = storedField(row.result, "outcome");
|
|
343
|
+
return outcome === "pull_request_opened" || outcome === "pull_request_updated";
|
|
344
|
+
});
|
|
345
|
+
// The root first, whatever the clock says.
|
|
346
|
+
return [...pushed.filter((row) => row.runId === rootRunId), ...pushed.filter((row) => row.runId !== rootRunId)].map((row) => row.runId);
|
|
347
|
+
}
|
|
348
|
+
async recordPullRequestDescription(input) {
|
|
349
|
+
const written = await this.db.codingRun.updateMany({
|
|
350
|
+
where: { runId: input.rootRunId, prDescriptionSha256: input.expectedSha256 },
|
|
351
|
+
data: { prDescriptionSha256: input.sha256, prDescriptionRunId: input.agentTextRunId },
|
|
352
|
+
});
|
|
353
|
+
return written.count === 1;
|
|
354
|
+
}
|
|
153
355
|
async claimProvisioning(runId, claimId) {
|
|
154
356
|
try {
|
|
155
357
|
return await this.db.$transaction(async (tx) => {
|
|
@@ -239,6 +441,52 @@ export class PrismaContainerExecutionStore {
|
|
|
239
441
|
data: { queuedAt: new Date() },
|
|
240
442
|
});
|
|
241
443
|
}
|
|
444
|
+
async requeueAfterCapacityRefusal(runId, owned) {
|
|
445
|
+
const holds = [
|
|
446
|
+
...(owned.claimId ? [{ jobBackend: PROVISIONING_BACKEND, jobHandle: owned.claimId }] : []),
|
|
447
|
+
...(owned.planned ? [{ jobBackend: owned.planned.backend, jobHandle: owned.planned.id }] : []),
|
|
448
|
+
];
|
|
449
|
+
if (holds.length === 0)
|
|
450
|
+
return "not_owned";
|
|
451
|
+
try {
|
|
452
|
+
return await this.db.$transaction(async (tx) => {
|
|
453
|
+
const run = await tx.run.findUnique({ where: { id: runId }, select: { startedAt: true } });
|
|
454
|
+
if (!run)
|
|
455
|
+
return "not_owned";
|
|
456
|
+
// queuedAt is the dispatch time, not now: claimProvisioning clears it on every claim, and a
|
|
457
|
+
// fresh value per refusal would keep a run the cluster refuses forever from timing out.
|
|
458
|
+
const released = await tx.codingRun.updateMany({
|
|
459
|
+
where: { runId, OR: holds, run: { status: { in: ["pending", "running"] } } },
|
|
460
|
+
data: { jobBackend: null, jobHandle: null, queuedAt: run.startedAt },
|
|
461
|
+
});
|
|
462
|
+
if (released.count !== 1)
|
|
463
|
+
return "not_owned";
|
|
464
|
+
// The pod never started, so its session served no request. A run has one session, and the
|
|
465
|
+
// next attempt creates its own: drop this one, but never one with spend recorded against it.
|
|
466
|
+
const session = await tx.codingProxySession.findUnique({
|
|
467
|
+
where: { runId },
|
|
468
|
+
select: { id: true, _count: { select: { requests: true } } },
|
|
469
|
+
});
|
|
470
|
+
if (session && session._count.requests > 0)
|
|
471
|
+
throw new NotRequeuedError("spend_recorded");
|
|
472
|
+
if (session)
|
|
473
|
+
await tx.codingProxySession.delete({ where: { id: session.id } });
|
|
474
|
+
const waiting = await tx.run.updateMany({
|
|
475
|
+
where: { id: runId, status: { in: ["pending", "running"] } },
|
|
476
|
+
data: { status: "pending", heartbeatAt: null },
|
|
477
|
+
});
|
|
478
|
+
// Ended (stopped, say) since the release above: roll the release back too.
|
|
479
|
+
if (waiting.count !== 1)
|
|
480
|
+
throw new NotRequeuedError("not_owned");
|
|
481
|
+
return "requeued";
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
catch (err) {
|
|
485
|
+
if (err instanceof NotRequeuedError)
|
|
486
|
+
return err.outcome;
|
|
487
|
+
throw err;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
242
490
|
async persistHandle(runId, claimId, handle) {
|
|
243
491
|
await this.db.$transaction(async (tx) => {
|
|
244
492
|
const coding = await tx.codingRun.findUnique({ where: { runId }, include: { run: true } });
|
|
@@ -290,6 +538,11 @@ export class PrismaContainerExecutionStore {
|
|
|
290
538
|
resultSchema: CODING_PROTOCOL_VERSION,
|
|
291
539
|
...(record.resultBranch ? { resultBranch: record.resultBranch } : {}),
|
|
292
540
|
...(record.baseSha ? { baseSha: record.baseSha } : {}),
|
|
541
|
+
...(record.dependencyCacheResult ? { dependencyCacheResult: record.dependencyCacheResult } : {}),
|
|
542
|
+
...(record.prDescriptionOutcome ? { prDescriptionOutcome: record.prDescriptionOutcome } : {}),
|
|
543
|
+
...(record.prDescriptionSha256 && record.prDescriptionRunId
|
|
544
|
+
? { prDescriptionSha256: record.prDescriptionSha256, prDescriptionRunId: record.prDescriptionRunId }
|
|
545
|
+
: {}),
|
|
293
546
|
},
|
|
294
547
|
});
|
|
295
548
|
await tx.run.update({
|
|
@@ -308,8 +561,33 @@ export class PrismaContainerExecutionStore {
|
|
|
308
561
|
await this.selfDefect({ ...finished, status, error: null, finishedAt });
|
|
309
562
|
}
|
|
310
563
|
}
|
|
564
|
+
async recordBudgetRefusal(runId, sessionId) {
|
|
565
|
+
const session = await this.db.codingProxySession.findUnique({
|
|
566
|
+
where: { id: sessionId },
|
|
567
|
+
select: { budgetRefusal: true },
|
|
568
|
+
});
|
|
569
|
+
// The proxy (a less-trusted role) wrote this: keep only well-formed amounts, model and turn.
|
|
570
|
+
const refusal = proxyRefusalDetails(session?.budgetRefusal);
|
|
571
|
+
if (!refusal)
|
|
572
|
+
return;
|
|
573
|
+
const existing = await this.db.runBudgetEvent.findFirst({
|
|
574
|
+
where: { runId, details: { path: ["source"], equals: "proxy" } },
|
|
575
|
+
select: { id: true },
|
|
576
|
+
});
|
|
577
|
+
if (existing)
|
|
578
|
+
return;
|
|
579
|
+
const coding = await this.db.codingRun.findUnique({ where: { runId }, select: { budgetLimitedBy: true } });
|
|
580
|
+
await recordBudgetEvent(this.db, runId, (coding?.budgetLimitedBy ?? "run"), refusal);
|
|
581
|
+
}
|
|
582
|
+
budgetEvent(runId) {
|
|
583
|
+
return loadBudgetEvent(this.db, runId);
|
|
584
|
+
}
|
|
585
|
+
topUpBudget(runId) {
|
|
586
|
+
return topUpCodingBudget(this.db, runId);
|
|
587
|
+
}
|
|
311
588
|
async terminate(runId, status, error, audit) {
|
|
312
589
|
const finishedAt = new Date();
|
|
590
|
+
let changed = false;
|
|
313
591
|
const agentId = await this.db.$transaction(async (tx) => {
|
|
314
592
|
const updated = await tx.run.updateMany({
|
|
315
593
|
where: { id: runId, status: { in: ["pending", "running"] } },
|
|
@@ -317,6 +595,7 @@ export class PrismaContainerExecutionStore {
|
|
|
317
595
|
});
|
|
318
596
|
if (!updated.count)
|
|
319
597
|
return null;
|
|
598
|
+
changed = true;
|
|
320
599
|
if (audit) {
|
|
321
600
|
await tx.codingRun.update({
|
|
322
601
|
where: { runId },
|
|
@@ -331,11 +610,17 @@ export class PrismaContainerExecutionStore {
|
|
|
331
610
|
// After commit, so the failure category is readable; cancelled/refused are skipped inside fileSelfDefect.
|
|
332
611
|
if (agentId)
|
|
333
612
|
await this.selfDefect({ id: runId, agentId, status, error, finishedAt });
|
|
613
|
+
return changed;
|
|
334
614
|
}
|
|
335
615
|
}
|
|
336
|
-
/**
|
|
616
|
+
/**
|
|
617
|
+
* Plain capabilities live only for the short provisioning window. A run with MCP tools keeps its
|
|
618
|
+
* MCP token here too, next to the capability, so the launcher delivers it the same way; both are
|
|
619
|
+
* deleted together.
|
|
620
|
+
*/
|
|
337
621
|
export class RunCapabilityVault {
|
|
338
622
|
values = new Map();
|
|
623
|
+
mcpTokens = new Map();
|
|
339
624
|
set(runId, capability) {
|
|
340
625
|
if (this.values.has(runId))
|
|
341
626
|
throw new Error("coding_capability_exists");
|
|
@@ -347,10 +632,23 @@ export class RunCapabilityVault {
|
|
|
347
632
|
throw new Error("coding_capability_unavailable");
|
|
348
633
|
return capability;
|
|
349
634
|
}
|
|
635
|
+
setMcpToken(runId, token) {
|
|
636
|
+
if (this.mcpTokens.has(runId))
|
|
637
|
+
throw new Error("coding_mcp_token_exists");
|
|
638
|
+
this.mcpTokens.set(runId, token);
|
|
639
|
+
}
|
|
640
|
+
/** The run's MCP token; undefined for a run without MCP tools (and once the launch is over). */
|
|
641
|
+
async mcpToken(runId) {
|
|
642
|
+
return this.mcpTokens.get(runId);
|
|
643
|
+
}
|
|
350
644
|
delete(runId) {
|
|
351
645
|
this.values.delete(runId);
|
|
646
|
+
this.mcpTokens.delete(runId);
|
|
352
647
|
}
|
|
353
648
|
}
|
|
649
|
+
/** A run's deduplicated served packages and refusals (see summarizeRegistryFetches), and its package audit. */
|
|
650
|
+
/** The poll interval, at most, while a run with an overrun limit is near its grant. */
|
|
651
|
+
export const TOP_UP_NEAR_POLL_MS = 1_000;
|
|
354
652
|
class PreflightError extends Error {
|
|
355
653
|
}
|
|
356
654
|
/** A refusal by repository authorization; its message is the failure category's prefix (repo_access_*). */
|
|
@@ -364,6 +662,11 @@ export class ContainerExecutor {
|
|
|
364
662
|
now;
|
|
365
663
|
observer;
|
|
366
664
|
startedAt = new Map();
|
|
665
|
+
/**
|
|
666
|
+
* When the cluster last refused each queued run's launch for capacity (this.now() ms). Per
|
|
667
|
+
* process: another replica's drain may start the run again inside the backoff.
|
|
668
|
+
*/
|
|
669
|
+
capacityRefusedAt = new Map();
|
|
367
670
|
constructor(options) {
|
|
368
671
|
this.options = options;
|
|
369
672
|
this.artifactRoot = resolve(options.artifactRoot);
|
|
@@ -415,6 +718,9 @@ export class ContainerExecutor {
|
|
|
415
718
|
const current = this.active.get(runId);
|
|
416
719
|
if (current)
|
|
417
720
|
return current;
|
|
721
|
+
// Refused for capacity a moment ago: keep its place and clock in the queue, untouched.
|
|
722
|
+
if (this.inCapacityBackoff(runId))
|
|
723
|
+
return;
|
|
418
724
|
this.startedAt.set(runId, this.now().getTime());
|
|
419
725
|
this.emit({ stage: "queued", runId });
|
|
420
726
|
const execution = this.execute(runId).finally(async () => {
|
|
@@ -434,10 +740,16 @@ export class ContainerExecutor {
|
|
|
434
740
|
return;
|
|
435
741
|
if (TERMINAL_STATUSES.has(run.status))
|
|
436
742
|
return this.cleanupTerminal(run);
|
|
437
|
-
this.emit({ stage: "stopping", runId, jobId: run.jobHandle?.id });
|
|
438
743
|
// Persist the cancellation fence first so a concurrent finisher cannot publish.
|
|
439
744
|
const failure = this.failure("cancelled", { cancelled: true });
|
|
440
|
-
|
|
745
|
+
// False when the run reached a terminal status after the load above: its finisher owns the
|
|
746
|
+
// cleanup and the continuation comment (recover() cleans up any terminal run the reconciler sees
|
|
747
|
+
// later), so stopping now would overwrite a good outcome. "stopping" is emitted only once this
|
|
748
|
+
// stop owns the run, so it is always followed by its terminal and cleanup events.
|
|
749
|
+
if (!(await this.options.store.terminate(runId, "cancelled", failure.error, failure.audit)))
|
|
750
|
+
return;
|
|
751
|
+
this.emit({ stage: "stopping", runId, jobId: run.jobHandle?.id });
|
|
752
|
+
await this.recordRefusal(runId, run.proxySessionId);
|
|
441
753
|
this.terminal(run, "cancelled", failure.audit);
|
|
442
754
|
if (run.proxySessionId)
|
|
443
755
|
await this.options.sessions.cancelSession(run.proxySessionId).catch(() => undefined);
|
|
@@ -448,10 +760,11 @@ export class ContainerExecutor {
|
|
|
448
760
|
this.options.capabilities.delete(runId);
|
|
449
761
|
const input = this.preflightForCleanup(run);
|
|
450
762
|
const workspace = input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null;
|
|
451
|
-
if (workspace)
|
|
763
|
+
if (workspace)
|
|
452
764
|
await this.options.vcs.cleanup(workspace).catch(() => undefined);
|
|
453
|
-
|
|
454
|
-
|
|
765
|
+
const target = workspace ?? this.continuationTargetOf(run);
|
|
766
|
+
if (target)
|
|
767
|
+
await this.options.vcs.notifyContinuationFinished?.(target, "failed", { agentName: run.agentName });
|
|
455
768
|
await rm(this.artifactPath(runId), { recursive: true, force: true }).catch(() => undefined);
|
|
456
769
|
this.emit({ stage: "cleanup", runId, jobId: run.jobHandle?.id, cleanupSucceeded: true });
|
|
457
770
|
}
|
|
@@ -479,6 +792,9 @@ export class ContainerExecutor {
|
|
|
479
792
|
await this.abandon(run, "coding_job_status_unavailable");
|
|
480
793
|
return { state: "lost", reason: "coding_job_status_unavailable" };
|
|
481
794
|
}
|
|
795
|
+
if (status.state === "pending" && status.capacityRefused && this.options.store.requeueAfterCapacityRefusal) {
|
|
796
|
+
return this.finishCapacityRequeue(run, run.jobHandle);
|
|
797
|
+
}
|
|
482
798
|
if (status.state === "pending" || status.state === "running") {
|
|
483
799
|
await this.options.store.heartbeat(run.runId);
|
|
484
800
|
return { state: "active" };
|
|
@@ -499,9 +815,12 @@ export class ContainerExecutor {
|
|
|
499
815
|
let sessionId = run.proxySessionId;
|
|
500
816
|
let handle = run.jobHandle;
|
|
501
817
|
let claimId = null;
|
|
818
|
+
let planned;
|
|
502
819
|
let spendEnabled = Boolean(sessionId);
|
|
503
820
|
try {
|
|
504
|
-
|
|
821
|
+
// A launched run resumed after a restart is checked the way recover() checks it: its cost so
|
|
822
|
+
// far may be past the limit (a reply can cost more than its hold), and it is still delivered.
|
|
823
|
+
const prepared = this.preflight(run, { launched: Boolean(handle) });
|
|
505
824
|
if (!handle) {
|
|
506
825
|
if (run.provisioningClaim)
|
|
507
826
|
return;
|
|
@@ -540,6 +859,20 @@ export class ContainerExecutor {
|
|
|
540
859
|
if (TERMINAL_STATUSES.has(beforeSession.status))
|
|
541
860
|
throw new Error("coding_run_no_longer_active");
|
|
542
861
|
const deadlineAt = new Date(this.now().getTime() + run.timeoutSec * 1_000);
|
|
862
|
+
// The agent's MCP tools, pinned for the run; only a run with some gets a token. The token is
|
|
863
|
+
// never logged: only its hash goes to the session, the value only into the vault.
|
|
864
|
+
// A Codex run's shell commands can read the token, so it gets read-only tools only; decided by
|
|
865
|
+
// the provider the run was dispatched with, which is what jobSpec launches. A failed read fails
|
|
866
|
+
// the run with a fixed code (the cause goes to the operator log only, redacted).
|
|
867
|
+
let mcpTools;
|
|
868
|
+
try {
|
|
869
|
+
mcpTools =
|
|
870
|
+
(await this.options.mcpToolPins?.(run.agentId, { readOnlyOnly: isCodexCoding(run.provider) })) ?? [];
|
|
871
|
+
}
|
|
872
|
+
catch (error) {
|
|
873
|
+
throw new Error(MCP_PINS_UNAVAILABLE_ERROR, { cause: error });
|
|
874
|
+
}
|
|
875
|
+
const mcpToken = mcpTools.length > 0 ? mintMcpToken() : undefined;
|
|
543
876
|
const session = await this.options.sessions.createSession({
|
|
544
877
|
runId,
|
|
545
878
|
credentialRef: this.credentialRef(run.provider),
|
|
@@ -548,18 +881,21 @@ export class ContainerExecutor {
|
|
|
548
881
|
deadlineAt,
|
|
549
882
|
budgetUsd: run.budgetUsd,
|
|
550
883
|
terms: this.modelTerms(run),
|
|
884
|
+
...(mcpToken ? { mcpTools, mcpTokenHash: mcpTokenHash(mcpToken) } : {}),
|
|
551
885
|
});
|
|
552
886
|
spendEnabled = true;
|
|
553
887
|
sessionId = session.id;
|
|
554
888
|
this.options.capabilities.set(runId, session.capability);
|
|
889
|
+
if (mcpToken)
|
|
890
|
+
this.options.capabilities.setMcpToken(runId, mcpToken);
|
|
555
891
|
const inputArtifact = await this.writeInput(run, deadlineAt, workspace);
|
|
556
892
|
const beforeLaunch = await this.requireCurrent(runId);
|
|
557
893
|
if (TERMINAL_STATUSES.has(beforeLaunch.status))
|
|
558
894
|
throw new Error("coding_run_no_longer_active");
|
|
559
|
-
const spec = this.jobSpec(run, inputArtifact);
|
|
895
|
+
const spec = this.jobSpec(run, inputArtifact, { mcpTools: Boolean(mcpToken) });
|
|
560
896
|
// Backends whose handle is derivable (Kubernetes) persist it first, so a crash between the
|
|
561
897
|
// launch and the write still leaves a handle `abandon()` can stop and remove the run with.
|
|
562
|
-
|
|
898
|
+
planned = this.options.jobs.plannedHandle?.(spec);
|
|
563
899
|
if (planned)
|
|
564
900
|
await this.options.store.persistHandle(runId, claimId, planned);
|
|
565
901
|
handle = await this.whileHeartbeating(runId, () => this.options.jobs.launch(spec));
|
|
@@ -567,12 +903,15 @@ export class ContainerExecutor {
|
|
|
567
903
|
throw new Error("coding_job_handle_mismatch");
|
|
568
904
|
}
|
|
569
905
|
await this.options.store.persistHandle(runId, claimId, handle);
|
|
906
|
+
this.capacityRefusedAt.delete(runId);
|
|
570
907
|
this.options.capabilities.delete(runId);
|
|
571
908
|
this.emit({ stage: "launched", runId, jobId: handle.id, budgetReservedUsd: run.budgetUsd });
|
|
572
909
|
}
|
|
573
910
|
let delay = this.options.pollMinMs ?? 250;
|
|
574
911
|
const maximumDelay = this.options.pollMaxMs ?? 5_000;
|
|
575
912
|
let observedRunning = false;
|
|
913
|
+
// Overrun from available room: asked on each poll until it is granted in full or refused once.
|
|
914
|
+
let asksForTopUps = run.budgetLimitUsd !== undefined && run.budgetLimitUsd > run.budgetUsd;
|
|
576
915
|
for (;;) {
|
|
577
916
|
const status = await this.options.jobs.status(handle);
|
|
578
917
|
if (status.state !== "pending" && status.state !== "running") {
|
|
@@ -584,6 +923,17 @@ export class ContainerExecutor {
|
|
|
584
923
|
this.emit({ stage: "running", runId, jobId: handle.id });
|
|
585
924
|
}
|
|
586
925
|
await this.options.store.heartbeat(runId);
|
|
926
|
+
if (asksForTopUps && this.options.store.topUpBudget) {
|
|
927
|
+
const topUp = await this.options.store.topUpBudget(runId).catch((error) => {
|
|
928
|
+
containerLog.warn({ runId, error: safeError(error) }, "coding run top-up failed; asking again next poll");
|
|
929
|
+
return { outcome: "not_needed", near: true };
|
|
930
|
+
});
|
|
931
|
+
if (topUp.outcome === "unavailable" || topUp.outcome === "closed")
|
|
932
|
+
asksForTopUps = false;
|
|
933
|
+
// Near its grant: poll often, so the next step lands before a request no longer fits.
|
|
934
|
+
else if (topUp.near)
|
|
935
|
+
delay = Math.min(delay, TOP_UP_NEAR_POLL_MS);
|
|
936
|
+
}
|
|
587
937
|
await this.sleep(delay);
|
|
588
938
|
delay = Math.min(maximumDelay, Math.max(delay + 1, Math.ceil(delay * 1.5)));
|
|
589
939
|
run = (await this.options.store.load(runId)) ?? run;
|
|
@@ -593,6 +943,34 @@ export class ContainerExecutor {
|
|
|
593
943
|
}
|
|
594
944
|
catch (error) {
|
|
595
945
|
this.options.capabilities.delete(runId);
|
|
946
|
+
if (error instanceof JobCapacityError && claimId && this.options.store.requeueAfterCapacityRefusal) {
|
|
947
|
+
// The cluster has no room for the pod (Kubernetes: the namespace quota): the run waits in
|
|
948
|
+
// the queue again rather than failing. The launcher already removed what it created, so
|
|
949
|
+
// there is no job to stop; nothing ran, so the session is dropped with the claim and a
|
|
950
|
+
// fresh one is made on the next attempt.
|
|
951
|
+
if (sessionId)
|
|
952
|
+
await this.options.sessions.cancelSession(sessionId).catch(() => undefined);
|
|
953
|
+
const requeued = await this.options.store.requeueAfterCapacityRefusal(runId, {
|
|
954
|
+
claimId,
|
|
955
|
+
...(planned ? { planned } : {}),
|
|
956
|
+
});
|
|
957
|
+
if (requeued !== "spend_recorded") {
|
|
958
|
+
if (workspace)
|
|
959
|
+
await this.options.vcs.cleanup(workspace).catch(() => undefined);
|
|
960
|
+
if (requeued === "requeued") {
|
|
961
|
+
// The next attempt writes its own input; this one names a session that is gone.
|
|
962
|
+
await rm(this.artifactPath(runId), { recursive: true, force: true }).catch(() => undefined);
|
|
963
|
+
this.recordCapacityRefusal(runId);
|
|
964
|
+
this.emit({ stage: "queued", runId });
|
|
965
|
+
containerLog.info({ runId }, "coding run waiting for capacity: the cluster refused its pod");
|
|
966
|
+
}
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
// Spend against a pod that never started: fail it like any launch error, and stop and remove
|
|
970
|
+
// the launcher's record through the planned handle so the run is never launched again.
|
|
971
|
+
if (planned)
|
|
972
|
+
handle = planned;
|
|
973
|
+
}
|
|
596
974
|
if (sessionId)
|
|
597
975
|
await this.options.sessions.cancelSession(sessionId).catch(() => undefined);
|
|
598
976
|
if (handle)
|
|
@@ -603,7 +981,8 @@ export class ContainerExecutor {
|
|
|
603
981
|
: !spendEnabled && error instanceof PreflightError
|
|
604
982
|
? "refused"
|
|
605
983
|
: "failed";
|
|
606
|
-
|
|
984
|
+
await this.recordRefusal(runId, sessionId);
|
|
985
|
+
const failure = outOfBudget ? await this.budgetFailure(runId, error) : this.failure(error);
|
|
607
986
|
this.emit({
|
|
608
987
|
stage: "stopping",
|
|
609
988
|
runId,
|
|
@@ -622,7 +1001,7 @@ export class ContainerExecutor {
|
|
|
622
1001
|
await this.options.vcs.cleanup(workspace).catch(() => undefined);
|
|
623
1002
|
await this.options.vcs.notifyContinuationFinished?.(workspace, outOfBudget ? "budget_exhausted" : "failed", {
|
|
624
1003
|
agentName: run.agentName,
|
|
625
|
-
...(outOfBudget ? { budgetSentence: this.budgetSentence(run) } : {}),
|
|
1004
|
+
...(outOfBudget ? { budgetSentence: await this.budgetSentence(run) } : {}),
|
|
626
1005
|
...(unreadyService ? { serviceSentence: serviceUnreadySentence([unreadyService]) } : {}),
|
|
627
1006
|
...(protectedPath ? { protectedPathSentence: protectedPathSentence(protectedPath) } : {}),
|
|
628
1007
|
});
|
|
@@ -675,51 +1054,82 @@ export class ContainerExecutor {
|
|
|
675
1054
|
* access. Throws RepoAccessError (refused before anything is cloned).
|
|
676
1055
|
*/
|
|
677
1056
|
async authorizeRepository(run, opts) {
|
|
1057
|
+
// Checked first: a local repository is otherwise authorized by the trusted roots alone.
|
|
1058
|
+
if (run.viaRepositoryLink && !run.repositoryLinkExists)
|
|
1059
|
+
throw new RepoAccessError("repo_access_not_authorized");
|
|
678
1060
|
const sameRepository = run.profileRepository !== null && run.profileRepository === run.repository;
|
|
1061
|
+
const authorizedVia = run.viaRepositoryLink
|
|
1062
|
+
? run.repositoryLinkAuthorizedVia
|
|
1063
|
+
: sameRepository
|
|
1064
|
+
? run.repositoryAuthorizedVia
|
|
1065
|
+
: "host_permission";
|
|
679
1066
|
const decision = await this.options.repoAccess.authorizeUse({
|
|
680
1067
|
ownerId: run.ownerId,
|
|
681
|
-
provider:
|
|
1068
|
+
provider: codeProviderOf(run.repository),
|
|
682
1069
|
repository: run.repository,
|
|
683
1070
|
required: requiredLevel("coding"),
|
|
684
|
-
authorizedVia
|
|
1071
|
+
authorizedVia,
|
|
685
1072
|
retryTransient: opts.inFlight,
|
|
686
1073
|
});
|
|
687
1074
|
if (!decision.ok)
|
|
688
1075
|
throw new RepoAccessError(`repo_access_${decision.reason}`);
|
|
689
1076
|
}
|
|
690
|
-
/**
|
|
691
|
-
|
|
1077
|
+
/**
|
|
1078
|
+
* The CodingRun's recorded issue as the Related section shows it (a Jira or
|
|
1079
|
+
* GitHub issue): control-plane data from the CodingRun row, key re-validated.
|
|
1080
|
+
*/
|
|
1081
|
+
recordedIssue(run) {
|
|
692
1082
|
const { issueProvider, issueKey } = run;
|
|
693
|
-
if (!issueProvider || !issueKey
|
|
1083
|
+
if (!issueProvider || !issueKey)
|
|
1084
|
+
return undefined;
|
|
1085
|
+
return issueReference(issueProvider, issueKey, (key) => this.options.issueUrl?.(issueProvider, key));
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* The originating tracker issue for the PR title/body. A GitHub issue is not
|
|
1089
|
+
* one: its line comes from issueSourceFor (`From issue #N`).
|
|
1090
|
+
*/
|
|
1091
|
+
issueFor(run) {
|
|
1092
|
+
if (run.issueProvider === "github")
|
|
694
1093
|
return {};
|
|
695
|
-
|
|
1094
|
+
const issue = this.recordedIssue(run);
|
|
1095
|
+
return issue ? { issue } : {};
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* The PR title/body's originating issue, whichever system it is in: the
|
|
1099
|
+
* CodingRun's tracker issue (issueFor), else the GitHub issue in the same
|
|
1100
|
+
* repository whose mention started the run tree (`From issue #N`). Read from
|
|
1101
|
+
* the root run, so a continuation's rebuilt body keeps the root's line.
|
|
1102
|
+
* Never throws: a failed lookup gives no issue line.
|
|
1103
|
+
*/
|
|
1104
|
+
async issueSourceFor(run) {
|
|
1105
|
+
const tracker = this.issueFor(run);
|
|
1106
|
+
if (tracker.issue || !this.options.store.linkedIssueSource)
|
|
1107
|
+
return tracker;
|
|
696
1108
|
try {
|
|
697
|
-
|
|
1109
|
+
const status = (await this.options.store.linkedIssueSource(run.rootCodingRunId ?? run.runId)).hostStatus;
|
|
1110
|
+
if (status?.provider === "github" &&
|
|
1111
|
+
status.issueMention === true &&
|
|
1112
|
+
status.repository.toLowerCase() === run.repository.toLowerCase() &&
|
|
1113
|
+
Number.isSafeInteger(status.number) &&
|
|
1114
|
+
status.number > 0) {
|
|
1115
|
+
return { issueNumber: status.number };
|
|
1116
|
+
}
|
|
698
1117
|
}
|
|
699
|
-
catch {
|
|
700
|
-
|
|
1118
|
+
catch (err) {
|
|
1119
|
+
containerLog.warn({ err, runId: run.runId }, "could not find the issue the run came from");
|
|
701
1120
|
}
|
|
702
|
-
|
|
703
|
-
? ISSUE_TRACKER_NAMES[issueProvider]
|
|
704
|
-
: undefined;
|
|
705
|
-
return {
|
|
706
|
-
issue: {
|
|
707
|
-
key: issueKey,
|
|
708
|
-
...(url?.startsWith("https://") ? { url } : {}),
|
|
709
|
-
...(trackerName ? { trackerName } : {}),
|
|
710
|
-
},
|
|
711
|
-
};
|
|
1121
|
+
return {};
|
|
712
1122
|
}
|
|
713
1123
|
/** The new PR's Related pull requests input: earlier PRs of the same request, then this one. Never throws. */
|
|
714
1124
|
async relatedFor(run) {
|
|
715
|
-
// A continuation
|
|
1125
|
+
// A continuation's Related block is preserved from the current body (the description update keeps it).
|
|
716
1126
|
if (run.rootCodingRunId || !this.options.store.relatedPullRequests)
|
|
717
1127
|
return {};
|
|
718
1128
|
try {
|
|
719
1129
|
const earlier = await this.options.store.relatedPullRequests(run.runId);
|
|
720
1130
|
if (earlier.length === 0)
|
|
721
1131
|
return {};
|
|
722
|
-
const
|
|
1132
|
+
const issue = this.recordedIssue(run);
|
|
723
1133
|
return {
|
|
724
1134
|
related: {
|
|
725
1135
|
entries: [
|
|
@@ -753,14 +1163,20 @@ export class ContainerExecutor {
|
|
|
753
1163
|
const entry = resolveCodingEntry(run.provider, run.model);
|
|
754
1164
|
return { version: entry.priceVersion, entry: parseStoredEntry(entry) };
|
|
755
1165
|
}
|
|
756
|
-
|
|
1166
|
+
/**
|
|
1167
|
+
* `launched`: the run's job was already launched (it is running, or done and
|
|
1168
|
+
* only its push is left). The cost check applies only before launch: one
|
|
1169
|
+
* reply can cost more than its hold, so a launched run may already be past
|
|
1170
|
+
* its limit, and its finished work is still delivered.
|
|
1171
|
+
*/
|
|
1172
|
+
preflight(run, options = {}) {
|
|
757
1173
|
try {
|
|
758
1174
|
if (run.status !== "pending" && run.status !== "running")
|
|
759
1175
|
throw new Error("coding_run_status_invalid");
|
|
760
1176
|
if (run.agentKind !== "coding" || !run.ownerId)
|
|
761
1177
|
throw new Error("coding_run_ownership_invalid");
|
|
762
1178
|
this.modelTerms(run);
|
|
763
|
-
if (!Number.isFinite(run.budgetUsd) || run.budgetUsd <= 0 || run.costUsd > run.budgetUsd) {
|
|
1179
|
+
if (!Number.isFinite(run.budgetUsd) || run.budgetUsd <= 0 || (!options.launched && run.costUsd > run.budgetUsd)) {
|
|
764
1180
|
throw new Error("coding_run_budget_invalid");
|
|
765
1181
|
}
|
|
766
1182
|
const profile = CodingProfileSchema.parse({
|
|
@@ -809,11 +1225,19 @@ export class ContainerExecutor {
|
|
|
809
1225
|
// "failed" and is only flipped right before a success or out-of-budget return.
|
|
810
1226
|
let outcome = "failed";
|
|
811
1227
|
let finishedSummary;
|
|
1228
|
+
// Set with a success that spent past the run's base budget; says so on the host.
|
|
1229
|
+
let overrun = {};
|
|
812
1230
|
// Set when a failed job's session relayed a model-provider failure; names it on the host.
|
|
813
1231
|
// Only a failed job: a transient provider error the worker retried past did not end the run.
|
|
814
1232
|
let providerClass;
|
|
815
1233
|
// Set when finalizeChanges refused the collected diff for touching a protected path; names it on the host.
|
|
816
1234
|
let protectedPath;
|
|
1235
|
+
// Set when a continuation's merge of its target branch left conflict markers behind; names the file on the host.
|
|
1236
|
+
let conflictMarkersPath;
|
|
1237
|
+
// Set with a success whose push merged the pull request's target branch; says what on the host.
|
|
1238
|
+
let merged = {};
|
|
1239
|
+
// Set when a run stopped at its budget still pushed its changes so far; says so on the host.
|
|
1240
|
+
let changesPushed;
|
|
817
1241
|
try {
|
|
818
1242
|
if (jobState !== "succeeded") {
|
|
819
1243
|
const collected = await this.options.jobs.collect(handle).catch(() => null);
|
|
@@ -822,7 +1246,8 @@ export class ContainerExecutor {
|
|
|
822
1246
|
// The worker usually fails outright when the proxy refuses a model
|
|
823
1247
|
// request for budget; the session records that refusal.
|
|
824
1248
|
if (await this.outOfBudget(sessionId)) {
|
|
825
|
-
|
|
1249
|
+
await this.recordRefusal(run.runId, sessionId);
|
|
1250
|
+
const failure = await this.budgetFailure(run.runId, `job_${reason}`, { issues: collected?.diagnosticIssues });
|
|
826
1251
|
await this.options.store.terminate(run.runId, "budget_exhausted", failure.error, failure.audit);
|
|
827
1252
|
this.terminal(run, "budget_exhausted", failure.audit);
|
|
828
1253
|
outcome = "budget_exhausted";
|
|
@@ -834,6 +1259,7 @@ export class ContainerExecutor {
|
|
|
834
1259
|
? this.providerFailureOf(run, provider, `job_${reason}`, { issues: collected?.diagnosticIssues })
|
|
835
1260
|
: this.failure(`job_${reason}`, { issues: collected?.diagnosticIssues });
|
|
836
1261
|
providerClass = provider?.class;
|
|
1262
|
+
await this.recordRefusal(run.runId, sessionId);
|
|
837
1263
|
await this.options.store.terminate(run.runId, status, failure.error, failure.audit);
|
|
838
1264
|
this.terminal(run, status, failure.audit);
|
|
839
1265
|
return;
|
|
@@ -842,94 +1268,124 @@ export class ContainerExecutor {
|
|
|
842
1268
|
this.emit({ stage: "collected", runId: run.runId, jobId: handle.id });
|
|
843
1269
|
if (collected.reason !== "completed" || !collected.resultArtifact)
|
|
844
1270
|
throw new Error("coding_result_missing");
|
|
845
|
-
const output =
|
|
1271
|
+
const output = parseCodingWorkerOutputJson(collected.resultArtifact);
|
|
846
1272
|
if (output.runId !== run.runId)
|
|
847
1273
|
throw new Error("coding_result_run_mismatch");
|
|
848
|
-
|
|
849
|
-
|
|
1274
|
+
const current = this.requireActive(await this.requireCurrent(run.runId));
|
|
1275
|
+
// Finished work is delivered even when it cost more than the run's limit (one reply
|
|
1276
|
+
// can cost more than its hold); the status comment then says so (overrunDetails).
|
|
1277
|
+
if (output.outcome === "budget_exhausted") {
|
|
850
1278
|
this.emit({ stage: "budget_cutoff", runId: run.runId, jobId: handle.id });
|
|
851
|
-
await this.
|
|
852
|
-
this.
|
|
1279
|
+
const kept = await this.keepStoppedChanges(run, handle, current, output, existingWorkspace);
|
|
1280
|
+
await this.recordRefusal(run.runId, sessionId);
|
|
1281
|
+
if (kept) {
|
|
1282
|
+
await this.options.store.complete(run.runId, "budget_exhausted", kept.result, kept.record);
|
|
1283
|
+
this.emit({ stage: kept.finalized.outcome, runId: run.runId, jobId: handle.id });
|
|
1284
|
+
this.terminal(kept.current, "budget_exhausted");
|
|
1285
|
+
changesPushed =
|
|
1286
|
+
kept.finalized.outcome === "branch_pushed" ? {} : { pullRequestNumber: kept.finalized.pullRequestNumber };
|
|
1287
|
+
if (kept.finalized.outcome === "pull_request_updated" && kept.finalized.targetMerge) {
|
|
1288
|
+
merged = { mergeSentence: mergeSentence(kept.finalized.targetMerge) };
|
|
1289
|
+
}
|
|
1290
|
+
finishedSummary = output.summary;
|
|
1291
|
+
}
|
|
1292
|
+
else {
|
|
1293
|
+
const latest = await this.requireCurrent(run.runId).catch(() => current);
|
|
1294
|
+
await this.options.store.complete(run.runId, "budget_exhausted", this.resultFor(output, latest, "budget_exhausted"), dependencyCacheRecord(output));
|
|
1295
|
+
this.terminal(latest, "budget_exhausted");
|
|
1296
|
+
}
|
|
853
1297
|
outcome = "budget_exhausted";
|
|
854
1298
|
return;
|
|
855
1299
|
}
|
|
856
|
-
|
|
857
|
-
|
|
1300
|
+
// A workspace that merged its target branch has that merge to push even when the agent changed nothing.
|
|
1301
|
+
if (output.outcome === "no_changes" && !(await this.mergedTargetBranch(current, existingWorkspace))) {
|
|
1302
|
+
// Before complete: the terminal hook (a no-change fix round's re-review) reads the refreshed body.
|
|
1303
|
+
const description = await this.refreshDescriptionInTime(current, output, existingWorkspace);
|
|
1304
|
+
await this.recordRefusal(run.runId, sessionId);
|
|
1305
|
+
await this.options.store.complete(run.runId, "succeeded", this.resultFor(output, current), {
|
|
1306
|
+
...dependencyCacheRecord(output),
|
|
1307
|
+
...description,
|
|
1308
|
+
});
|
|
858
1309
|
this.terminal(current, "succeeded");
|
|
859
1310
|
outcome = "succeeded";
|
|
1311
|
+
overrun = this.overrunDetails(current);
|
|
860
1312
|
finishedSummary = output.summary;
|
|
861
1313
|
return;
|
|
862
1314
|
}
|
|
863
|
-
const
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
await this.options.store.complete(run.runId, "budget_exhausted", this.resultFor(output, current, "budget_exhausted"));
|
|
873
|
-
this.terminal(current, "budget_exhausted");
|
|
874
|
-
outcome = "budget_exhausted";
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
// One more (usually cached) check right before anything is pushed: access
|
|
878
|
-
// revoked while the run was working stops the push.
|
|
879
|
-
await this.authorizeRepository(current, { inFlight: true });
|
|
880
|
-
const report = await this.loadRegistryReport(run.runId);
|
|
881
|
-
const finalized = await this.options.vcs.finalizeChanges(workspace, {
|
|
882
|
-
summary: output.summary,
|
|
883
|
-
tests: output.tests,
|
|
884
|
-
tag: output.tag,
|
|
885
|
-
...report,
|
|
886
|
-
...this.issueFor(current),
|
|
887
|
-
...(await this.relatedFor(current)),
|
|
888
|
-
});
|
|
1315
|
+
const pushed = await this.pushChanges(run, handle, current, output, output.summary, existingWorkspace);
|
|
1316
|
+
const { finalized, workspace } = pushed;
|
|
1317
|
+
// An empty diff pushes nothing, so it refreshes the description the way a
|
|
1318
|
+
// worker no_changes does, before complete (the terminal hook reads the body).
|
|
1319
|
+
const description = finalized.outcome === "no_changes"
|
|
1320
|
+
? await this.refreshDescriptionInTime(pushed.current, output, workspace)
|
|
1321
|
+
: await this.recordDescription(pushed.current, pushed.descriptionState, finalized.outcome === "pull_request_opened" || finalized.outcome === "pull_request_updated"
|
|
1322
|
+
? finalized.description
|
|
1323
|
+
: undefined);
|
|
889
1324
|
// A pushed branch (local repository) is a successful run with no pull
|
|
890
1325
|
// request; its branch is recorded on the run for the operator to merge.
|
|
891
|
-
const result = this.resultFor(output, current, finalized.outcome, finalized);
|
|
1326
|
+
const result = this.resultFor(output, pushed.current, finalized.outcome, finalized);
|
|
1327
|
+
await this.recordRefusal(run.runId, sessionId);
|
|
892
1328
|
await this.options.store.complete(run.runId, "succeeded", result, {
|
|
893
|
-
|
|
894
|
-
...(finalized
|
|
1329
|
+
...dependencyCacheRecord(output),
|
|
1330
|
+
...pushedRecord(finalized),
|
|
1331
|
+
...description,
|
|
895
1332
|
});
|
|
896
|
-
if (finalized.outcome
|
|
897
|
-
finalized.outcome === "pull_request_updated" ||
|
|
898
|
-
finalized.outcome === "branch_pushed") {
|
|
1333
|
+
if (finalized.outcome !== "no_changes") {
|
|
899
1334
|
this.emit({ stage: finalized.outcome, runId: run.runId, jobId: handle.id });
|
|
900
1335
|
}
|
|
901
|
-
this.terminal(current, "succeeded");
|
|
1336
|
+
this.terminal(pushed.current, "succeeded");
|
|
902
1337
|
outcome = "succeeded";
|
|
1338
|
+
overrun = this.overrunDetails(pushed.current);
|
|
1339
|
+
if (finalized.outcome === "pull_request_updated" && finalized.targetMerge) {
|
|
1340
|
+
merged = { mergeSentence: mergeSentence(finalized.targetMerge) };
|
|
1341
|
+
}
|
|
903
1342
|
finishedSummary = output.summary;
|
|
904
1343
|
}
|
|
905
1344
|
catch (error) {
|
|
906
|
-
if (
|
|
907
|
-
|
|
1345
|
+
if (error instanceof Error && error.message === "coding_run_no_longer_active") {
|
|
1346
|
+
// Stopped while its result was collected: the stop already wrote the terminal row.
|
|
1347
|
+
containerLog.info({ runId: run.runId }, "coding run was stopped before its result was published");
|
|
1348
|
+
}
|
|
1349
|
+
else if (await this.outOfBudget(sessionId)) {
|
|
1350
|
+
await this.recordRefusal(run.runId, sessionId);
|
|
1351
|
+
const failure = await this.budgetFailure(run.runId, error);
|
|
908
1352
|
await this.options.store.terminate(run.runId, "budget_exhausted", failure.error, failure.audit);
|
|
909
1353
|
this.terminal(run, "budget_exhausted", failure.audit);
|
|
910
1354
|
outcome = "budget_exhausted";
|
|
911
1355
|
}
|
|
912
1356
|
else {
|
|
1357
|
+
await this.recordRefusal(run.runId, sessionId);
|
|
913
1358
|
const failure = this.failure(error);
|
|
914
1359
|
await this.options.store.terminate(run.runId, "failed", failure.error, failure.audit);
|
|
915
1360
|
this.terminal(run, "failed", failure.audit);
|
|
916
1361
|
protectedPath = protectedPathFromError(error) ?? undefined;
|
|
1362
|
+
conflictMarkersPath = conflictMarkersPathFromError(error) ?? undefined;
|
|
917
1363
|
}
|
|
918
1364
|
}
|
|
919
1365
|
finally {
|
|
1366
|
+
if (run.sessionResume)
|
|
1367
|
+
await this.recordTranscript(run, handle);
|
|
920
1368
|
await this.options.jobs.remove(handle).catch(() => undefined);
|
|
921
1369
|
const input = this.preflightForCleanup(run);
|
|
922
1370
|
const workspace = existingWorkspace ?? (input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null);
|
|
923
|
-
if (workspace)
|
|
1371
|
+
if (workspace)
|
|
924
1372
|
await this.options.vcs.cleanup(workspace).catch(() => undefined);
|
|
925
|
-
|
|
1373
|
+
// A run another process launched has no workspace here; its pull request still gets the final status.
|
|
1374
|
+
const target = workspace ?? this.continuationTargetOf(run);
|
|
1375
|
+
if (target) {
|
|
1376
|
+
await this.options.vcs.notifyContinuationFinished?.(target, outcome, {
|
|
926
1377
|
summary: finishedSummary,
|
|
927
1378
|
agentName: run.agentName,
|
|
928
|
-
...(outcome === "
|
|
1379
|
+
...(outcome === "succeeded" ? { ...merged, ...overrun } : {}),
|
|
1380
|
+
...(outcome === "budget_exhausted" ? { budgetSentence: await this.budgetSentence(run) } : {}),
|
|
1381
|
+
...(outcome === "budget_exhausted" && changesPushed ? { changesPushed, ...merged } : {}),
|
|
929
1382
|
...(outcome === "failed" && providerClass ? { providerSentence: providerSentence(providerClass) } : {}),
|
|
930
1383
|
...(outcome === "failed" && protectedPath
|
|
931
1384
|
? { protectedPathSentence: protectedPathSentence(protectedPath) }
|
|
932
1385
|
: {}),
|
|
1386
|
+
...(outcome === "failed" && conflictMarkersPath
|
|
1387
|
+
? { conflictMarkersSentence: conflictMarkersSentence(conflictMarkersPath) }
|
|
1388
|
+
: {}),
|
|
933
1389
|
});
|
|
934
1390
|
}
|
|
935
1391
|
await rm(this.artifactPath(run.runId), { recursive: true, force: true }).catch(() => undefined);
|
|
@@ -937,6 +1393,414 @@ export class ContainerExecutor {
|
|
|
937
1393
|
await this.notifyTerminal(run.runId);
|
|
938
1394
|
}
|
|
939
1395
|
}
|
|
1396
|
+
/** Whether the run's workspace merged its pull request's target branch (PreparedWorkspace.targetMerge). */
|
|
1397
|
+
async mergedTargetBranch(run, existingWorkspace) {
|
|
1398
|
+
if (!run.rootCodingRunId)
|
|
1399
|
+
return false;
|
|
1400
|
+
if (existingWorkspace)
|
|
1401
|
+
return Boolean(existingWorkspace.targetMerge);
|
|
1402
|
+
try {
|
|
1403
|
+
return Boolean((await this.options.vcs.recoverWorkspace(this.preflight(run, { launched: true })))?.targetMerge);
|
|
1404
|
+
}
|
|
1405
|
+
catch {
|
|
1406
|
+
return false;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Pushes a finished job's changes: copies the job's workspace into the run's,
|
|
1411
|
+
* then, right before finalizeChanges pushes, checks again that the owner still
|
|
1412
|
+
* has access and that the run was not stopped meanwhile. Shared by a finished
|
|
1413
|
+
* run and one stopped at its budget; throws whatever stops the push.
|
|
1414
|
+
*/
|
|
1415
|
+
async pushChanges(run, handle, collectedFor, output, summary, existingWorkspace, options = {}) {
|
|
1416
|
+
const preparedInput = this.preflight(collectedFor, { launched: true });
|
|
1417
|
+
const workspace = existingWorkspace ?? (await this.options.vcs.recoverWorkspace(preparedInput));
|
|
1418
|
+
if (!workspace)
|
|
1419
|
+
throw new Error("coding_workspace_lost");
|
|
1420
|
+
await this.options.jobs.materializeWorkspace(handle, workspace.workspacePath);
|
|
1421
|
+
await normalizeCollectedLockfiles(collectedFor.provider, workspace.workspacePath);
|
|
1422
|
+
const current = await this.requireCurrent(run.runId);
|
|
1423
|
+
// One more (usually cached) check right before anything is pushed: access
|
|
1424
|
+
// revoked while the run was working stops the push.
|
|
1425
|
+
await this.authorizeRepository(current, { inFlight: true });
|
|
1426
|
+
// And a run stopped while its result was collected (stop_run, a cancelled task) pushes nothing.
|
|
1427
|
+
this.requireActive(await this.requireCurrent(run.runId));
|
|
1428
|
+
const report = await this.pullRequestRegistryReport(current);
|
|
1429
|
+
// keepDescription: a continuation's existing description is left as it is (no update, no proposal).
|
|
1430
|
+
const descriptionState = options.keepDescription ? null : await this.descriptionStateFor(current);
|
|
1431
|
+
const finalized = await this.options.vcs.finalizeChanges(workspace, {
|
|
1432
|
+
summary,
|
|
1433
|
+
tests: output.tests,
|
|
1434
|
+
tag: output.tag,
|
|
1435
|
+
...report,
|
|
1436
|
+
...restoredDependenciesFor(output),
|
|
1437
|
+
...(await this.issueSourceFor(current)),
|
|
1438
|
+
...(await this.relatedFor(current)),
|
|
1439
|
+
...(descriptionState && "baseline" in descriptionState
|
|
1440
|
+
? { description: { baseline: descriptionState.baseline } }
|
|
1441
|
+
: {}),
|
|
1442
|
+
});
|
|
1443
|
+
return { current, workspace, finalized, descriptionState };
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* A run stopped at its budget whose worker exited cleanly keeps its changes so
|
|
1447
|
+
* far: they are pushed as the usual draft (or branch), with a summary that says
|
|
1448
|
+
* the work is unfinished. A fresh pull request's description starts with that
|
|
1449
|
+
* summary; a continuation leaves its pull request's description alone (the
|
|
1450
|
+
* status comment says the run stopped), and pushes nothing onto a pull request
|
|
1451
|
+
* that is no longer a draft. Null when there was nothing to push or the push
|
|
1452
|
+
* failed or was skipped for any reason: the run then ends out of budget without
|
|
1453
|
+
* them. Once pushed, the pushed outcome is always returned. Throws only when the
|
|
1454
|
+
* run was stopped meanwhile (the stop wrote its terminal row).
|
|
1455
|
+
*/
|
|
1456
|
+
async keepStoppedChanges(run, handle, current, output, existingWorkspace) {
|
|
1457
|
+
const summary = budgetStoppedSummary(output.summary, current.request);
|
|
1458
|
+
const continuesPullRequest = Boolean(current.rootCodingRunId) && hasPullRequestDescription(current);
|
|
1459
|
+
let pushed;
|
|
1460
|
+
try {
|
|
1461
|
+
let workspace = existingWorkspace;
|
|
1462
|
+
if (continuesPullRequest) {
|
|
1463
|
+
workspace ??= await this.options.vcs.recoverWorkspace(this.preflight(current, { launched: true }));
|
|
1464
|
+
if (!workspace)
|
|
1465
|
+
throw new Error("coding_workspace_lost");
|
|
1466
|
+
if (!(await this.continuationStillDraft(current, workspace))) {
|
|
1467
|
+
containerLog.info({ runId: run.runId }, "a run stopped at its budget pushes nothing onto a pull request that is no longer a draft");
|
|
1468
|
+
return null;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
pushed = await this.pushChanges(run, handle, current, output, summary, workspace, {
|
|
1472
|
+
keepDescription: continuesPullRequest,
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
catch (error) {
|
|
1476
|
+
if (error instanceof Error && error.message === "coding_run_no_longer_active")
|
|
1477
|
+
throw error;
|
|
1478
|
+
containerLog.warn({ runId: run.runId, reason: describeFailure(error) }, "could not push the changes of a run stopped at its budget; it ends without them");
|
|
1479
|
+
return null;
|
|
1480
|
+
}
|
|
1481
|
+
const { finalized } = pushed;
|
|
1482
|
+
if (finalized.outcome === "no_changes")
|
|
1483
|
+
return null;
|
|
1484
|
+
// Pushed: from here on the run records the pushed outcome whatever else fails.
|
|
1485
|
+
const description = await this.recordDescription(pushed.current, pushed.descriptionState, finalized.outcome === "pull_request_opened" || finalized.outcome === "pull_request_updated"
|
|
1486
|
+
? finalized.description
|
|
1487
|
+
: undefined).catch(() => ({}));
|
|
1488
|
+
let result;
|
|
1489
|
+
try {
|
|
1490
|
+
result = this.resultFor({ ...output, summary }, pushed.current, finalized.outcome, finalized);
|
|
1491
|
+
}
|
|
1492
|
+
catch (error) {
|
|
1493
|
+
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");
|
|
1494
|
+
result = this.resultFor({
|
|
1495
|
+
...output,
|
|
1496
|
+
summary: budgetStoppedSummary(WORKER_BUDGET_EXHAUSTED_SUMMARY, current.request),
|
|
1497
|
+
tests: [],
|
|
1498
|
+
tag: undefined,
|
|
1499
|
+
}, pushed.current, finalized.outcome, finalized);
|
|
1500
|
+
}
|
|
1501
|
+
return {
|
|
1502
|
+
current: pushed.current,
|
|
1503
|
+
finalized,
|
|
1504
|
+
result,
|
|
1505
|
+
record: { ...dependencyCacheRecord(output), ...pushedRecord(finalized), ...description },
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* The pull request description a continuation run is about to replace, for
|
|
1510
|
+
* its task. Undefined for a fresh run or when it cannot be loaded: the run
|
|
1511
|
+
* goes ahead without the note.
|
|
1512
|
+
*/
|
|
1513
|
+
async currentDescriptionText(run) {
|
|
1514
|
+
const store = this.options.store;
|
|
1515
|
+
if (!run.rootCodingRunId || !hasPullRequestDescription(run) || !store.pullRequestDescriptionBaseline) {
|
|
1516
|
+
return undefined;
|
|
1517
|
+
}
|
|
1518
|
+
try {
|
|
1519
|
+
return (await store.pullRequestDescriptionBaseline(run.rootCodingRunId)).previousAgentText;
|
|
1520
|
+
}
|
|
1521
|
+
catch (err) {
|
|
1522
|
+
containerLog.debug({ err, runId: run.runId }, "could not load the pull request description for the task");
|
|
1523
|
+
return undefined;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* The issue a continuation's pull request was opened for, read for its task
|
|
1528
|
+
* the way the first run had it: the root run's Jira issue, else the GitHub
|
|
1529
|
+
* issue whose mention started the work (same repository), else the issue the
|
|
1530
|
+
* description wardby last wrote closes (same repository). Each candidate is
|
|
1531
|
+
* read in turn until one is an issue. Undefined for a fresh or local run, or
|
|
1532
|
+
* on any failure: the run goes ahead without it.
|
|
1533
|
+
*/
|
|
1534
|
+
async linkedIssueFor(run, description) {
|
|
1535
|
+
const store = this.options.store;
|
|
1536
|
+
const read = this.options.readLinkedIssue;
|
|
1537
|
+
if (!run.rootCodingRunId || !hasPullRequestDescription(run) || !read || !store.linkedIssueSource)
|
|
1538
|
+
return undefined;
|
|
1539
|
+
let timer;
|
|
1540
|
+
const timedOut = new Promise((resolve) => {
|
|
1541
|
+
timer = setTimeout(() => {
|
|
1542
|
+
containerLog.warn({ runId: run.runId }, "reading the linked issue timed out; continuing without it");
|
|
1543
|
+
resolve(undefined);
|
|
1544
|
+
}, LINKED_ISSUE_READ_TIMEOUT_MS);
|
|
1545
|
+
});
|
|
1546
|
+
try {
|
|
1547
|
+
return await Promise.race([this.readLinkedIssue(run, run.rootCodingRunId, description, read), timedOut]);
|
|
1548
|
+
}
|
|
1549
|
+
catch (err) {
|
|
1550
|
+
containerLog.warn({ err, runId: run.runId }, "could not read the linked issue; continuing without it");
|
|
1551
|
+
return undefined;
|
|
1552
|
+
}
|
|
1553
|
+
finally {
|
|
1554
|
+
clearTimeout(timer);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
async readLinkedIssue(run, rootRunId, description, read) {
|
|
1558
|
+
const source = await this.options.store.linkedIssueSource(rootRunId);
|
|
1559
|
+
const candidates = [];
|
|
1560
|
+
if (source.issueProvider === "jira" && source.issueKey && ISSUE_KEY.test(source.issueKey)) {
|
|
1561
|
+
candidates.push({ provider: "jira", key: source.issueKey });
|
|
1562
|
+
}
|
|
1563
|
+
const status = source.hostStatus;
|
|
1564
|
+
if (status?.provider === "github" &&
|
|
1565
|
+
status.repository.toLowerCase() === run.repository.toLowerCase() &&
|
|
1566
|
+
Number.isSafeInteger(status.number) &&
|
|
1567
|
+
status.number > 0) {
|
|
1568
|
+
candidates.push({ provider: "github", repository: run.repository, number: status.number });
|
|
1569
|
+
}
|
|
1570
|
+
// The closing keyword names a GitHub issue: no other host's repository has one to read.
|
|
1571
|
+
const closed = codeProviderOf(run.repository) === "github" ? closingIssueNumber(description, run.repository) : undefined;
|
|
1572
|
+
if (closed !== undefined && !candidates.some((ref) => ref.provider === "github" && ref.number === closed)) {
|
|
1573
|
+
candidates.push({ provider: "github", repository: run.repository, number: closed });
|
|
1574
|
+
}
|
|
1575
|
+
for (const ref of candidates) {
|
|
1576
|
+
let issue;
|
|
1577
|
+
try {
|
|
1578
|
+
issue = await read(ref);
|
|
1579
|
+
}
|
|
1580
|
+
catch (err) {
|
|
1581
|
+
// The next candidate may still be readable.
|
|
1582
|
+
containerLog.warn({ err, runId: run.runId, issue: ref }, "could not read a linked issue candidate");
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
if (!issue)
|
|
1586
|
+
continue;
|
|
1587
|
+
return ref.provider === "github"
|
|
1588
|
+
? { kind: "github", number: ref.number, title: issue.title, body: issue.body }
|
|
1589
|
+
: { kind: "jira", key: ref.key, title: issue.title, body: issue.body };
|
|
1590
|
+
}
|
|
1591
|
+
return undefined;
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* A fix round's description baseline (null: no description flow -- a fresh
|
|
1595
|
+
* run, or a store without it). Never throws: a load failure is recorded as
|
|
1596
|
+
* skipped:error and the push goes ahead without a description update.
|
|
1597
|
+
*/
|
|
1598
|
+
/**
|
|
1599
|
+
* Whether a continuation's pull request is still a draft: the host's answer
|
|
1600
|
+
* when it has one, else the root run's record of wardby marking it ready.
|
|
1601
|
+
* A failed record lookup counts as not a draft (nothing unfinished is pushed).
|
|
1602
|
+
*/
|
|
1603
|
+
async continuationStillDraft(run, workspace) {
|
|
1604
|
+
const draft = await this.options.vcs.continuationPullRequestDraft?.(workspace).catch(() => undefined);
|
|
1605
|
+
if (draft !== undefined)
|
|
1606
|
+
return draft;
|
|
1607
|
+
const store = this.options.store;
|
|
1608
|
+
if (!run.rootCodingRunId || !store.pullRequestMarkedReady)
|
|
1609
|
+
return true;
|
|
1610
|
+
try {
|
|
1611
|
+
return !(await store.pullRequestMarkedReady(run.rootCodingRunId));
|
|
1612
|
+
}
|
|
1613
|
+
catch (err) {
|
|
1614
|
+
containerLog.warn({ err, runId: run.runId }, "could not read whether the pull request was marked ready");
|
|
1615
|
+
return false;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
async descriptionStateFor(run) {
|
|
1619
|
+
const store = this.options.store;
|
|
1620
|
+
if (!run.rootCodingRunId || !hasPullRequestDescription(run) || !store.pullRequestDescriptionBaseline)
|
|
1621
|
+
return null;
|
|
1622
|
+
let stored;
|
|
1623
|
+
try {
|
|
1624
|
+
stored = await store.pullRequestDescriptionBaseline(run.rootCodingRunId);
|
|
1625
|
+
}
|
|
1626
|
+
catch (err) {
|
|
1627
|
+
containerLog.warn({ err, runId: run.runId }, "could not load the pull request description baseline");
|
|
1628
|
+
return { failed: true };
|
|
1629
|
+
}
|
|
1630
|
+
const { legacySource, ...baseline } = stored;
|
|
1631
|
+
if (baseline.recordedSha256 !== null || !legacySource)
|
|
1632
|
+
return { baseline };
|
|
1633
|
+
const legacySha256 = await this.legacyDescriptionSha256(run.rootCodingRunId, legacySource);
|
|
1634
|
+
return { baseline: legacySha256 ? { ...baseline, legacySha256 } : baseline };
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* A legacy pull request's description as its root run wrote it, rebuilt from
|
|
1638
|
+
* stored records (the final message, tests, packages, dependency cache line
|
|
1639
|
+
* and issue line; the Related section is not hashed), and hashed. Undefined
|
|
1640
|
+
* when the root stored no final message, or on any failure: the description
|
|
1641
|
+
* then counts as edited, the safe direction. A lock file line the root's
|
|
1642
|
+
* refusal warning may have carried is not stored, so such a description
|
|
1643
|
+
* never matches.
|
|
1644
|
+
*/
|
|
1645
|
+
async legacyDescriptionSha256(rootRunId, source) {
|
|
1646
|
+
if (!source.summary)
|
|
1647
|
+
return undefined;
|
|
1648
|
+
try {
|
|
1649
|
+
const tests = storedTests(source.tests);
|
|
1650
|
+
const restored = source.dependencyCache === undefined
|
|
1651
|
+
? []
|
|
1652
|
+
: restoredDependencyFolders(sanitizeDependencyCacheReport(source.dependencyCache));
|
|
1653
|
+
const body = pullRequestBody({
|
|
1654
|
+
repository: source.repository,
|
|
1655
|
+
baseRef: source.baseRef,
|
|
1656
|
+
headRef: source.headRef,
|
|
1657
|
+
runId: rootRunId,
|
|
1658
|
+
summary: source.summary,
|
|
1659
|
+
...(tests.length > 0 ? { tests } : {}),
|
|
1660
|
+
...(await this.loadRegistryReport(rootRunId)),
|
|
1661
|
+
...(restored.length > 0 ? { restoredDependencies: restored } : {}),
|
|
1662
|
+
...this.issueFor({ issueProvider: source.issueProvider ?? null, issueKey: source.issueKey ?? null }),
|
|
1663
|
+
});
|
|
1664
|
+
return descriptionSha256(body);
|
|
1665
|
+
}
|
|
1666
|
+
catch (err) {
|
|
1667
|
+
containerLog.debug({ err, rootRunId }, "could not rebuild a legacy pull request description");
|
|
1668
|
+
return undefined;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* The Packages section's input: on a continuation, every package installed
|
|
1673
|
+
* by the runs that pushed to the pull request plus this run (merged in run
|
|
1674
|
+
* order); refusals are this run's only, since they describe the current
|
|
1675
|
+
* state. A fresh run, or a failed listing, reports this run's alone.
|
|
1676
|
+
*/
|
|
1677
|
+
async pullRequestRegistryReport(run) {
|
|
1678
|
+
const own = await this.loadRegistryReport(run.runId);
|
|
1679
|
+
const store = this.options.store;
|
|
1680
|
+
if (!run.rootCodingRunId || !hasPullRequestDescription(run) || !store.pullRequestRunIds)
|
|
1681
|
+
return own;
|
|
1682
|
+
let earlier;
|
|
1683
|
+
try {
|
|
1684
|
+
earlier = (await store.pullRequestRunIds(run.rootCodingRunId)).filter((id) => id !== run.runId);
|
|
1685
|
+
}
|
|
1686
|
+
catch (err) {
|
|
1687
|
+
containerLog.warn({ err, runId: run.runId }, "could not list the pull request's runs; listing this run's packages");
|
|
1688
|
+
return own;
|
|
1689
|
+
}
|
|
1690
|
+
const reports = await Promise.all(earlier.map((id) => this.loadRegistryReport(id)));
|
|
1691
|
+
const packages = mergePackages([...reports.map((report) => report.packages ?? []), own.packages ?? []]);
|
|
1692
|
+
// Package audit mode is cumulative like packages: the banner stays on the
|
|
1693
|
+
// pull request once any of its runs had the mode.
|
|
1694
|
+
const packageAudit = mergePackageAuditReports([...reports.map((report) => report.packageAudit), own.packageAudit]);
|
|
1695
|
+
return {
|
|
1696
|
+
...(packages.length > 0 ? { packages } : {}),
|
|
1697
|
+
...(own.packageRefusals ? { packageRefusals: own.packageRefusals } : {}),
|
|
1698
|
+
...(packageAudit ? { packageAudit } : {}),
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* The run's description columns for store.complete. A fresh run that wrote
|
|
1703
|
+
* its PR records the hash on its own (root) row; a fix round that wrote or
|
|
1704
|
+
* kept wardby's text compare-and-sets the root's hash. Never throws.
|
|
1705
|
+
*/
|
|
1706
|
+
async recordDescription(run, state, outcome) {
|
|
1707
|
+
if (!hasPullRequestDescription(run))
|
|
1708
|
+
return {};
|
|
1709
|
+
if (!run.rootCodingRunId) {
|
|
1710
|
+
return outcome?.kind === "written"
|
|
1711
|
+
? { prDescriptionOutcome: "written", prDescriptionSha256: outcome.sha256, prDescriptionRunId: run.runId }
|
|
1712
|
+
: {};
|
|
1713
|
+
}
|
|
1714
|
+
if (!state)
|
|
1715
|
+
return {};
|
|
1716
|
+
if ("failed" in state)
|
|
1717
|
+
return { prDescriptionOutcome: "skipped:error" };
|
|
1718
|
+
if (!outcome || outcome.kind === "written")
|
|
1719
|
+
return {};
|
|
1720
|
+
const label = descriptionOutcomeLabel(outcome);
|
|
1721
|
+
if (outcome.kind !== "replaced" && outcome.kind !== "refreshed" && outcome.kind !== "unchanged") {
|
|
1722
|
+
return { prDescriptionOutcome: label };
|
|
1723
|
+
}
|
|
1724
|
+
const store = this.options.store;
|
|
1725
|
+
if (!store.recordPullRequestDescription)
|
|
1726
|
+
return { prDescriptionOutcome: label };
|
|
1727
|
+
try {
|
|
1728
|
+
const won = await store.recordPullRequestDescription({
|
|
1729
|
+
rootRunId: run.rootCodingRunId,
|
|
1730
|
+
expectedSha256: state.baseline.recordedSha256,
|
|
1731
|
+
sha256: outcome.sha256,
|
|
1732
|
+
agentTextRunId: outcome.agentTextRunId,
|
|
1733
|
+
});
|
|
1734
|
+
// A lost race leaves the root's hash as the other write left it: the next
|
|
1735
|
+
// round sees a mismatch and falls back to a comment, the safe direction.
|
|
1736
|
+
if (!won) {
|
|
1737
|
+
containerLog.warn({ runId: run.runId, rootRunId: run.rootCodingRunId }, "pull request description hash changed meanwhile; not recorded");
|
|
1738
|
+
}
|
|
1739
|
+
return { prDescriptionOutcome: label };
|
|
1740
|
+
}
|
|
1741
|
+
catch (err) {
|
|
1742
|
+
containerLog.warn({ err, runId: run.runId }, "could not record the pull request description hash");
|
|
1743
|
+
return { prDescriptionOutcome: "skipped:error" };
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* refreshDescriptionWithoutPush, capped at DESCRIPTION_REFRESH_TIMEOUT_MS so a
|
|
1748
|
+
* slow host cannot hold up store.complete; a timeout is skipped:error.
|
|
1749
|
+
*/
|
|
1750
|
+
async refreshDescriptionInTime(run, output, existingWorkspace) {
|
|
1751
|
+
let timer;
|
|
1752
|
+
const timedOut = new Promise((resolve) => {
|
|
1753
|
+
timer = setTimeout(() => {
|
|
1754
|
+
containerLog.warn({ runId: run.runId, timeoutMs: DESCRIPTION_REFRESH_TIMEOUT_MS }, "pull request description refresh timed out");
|
|
1755
|
+
resolve({ prDescriptionOutcome: "skipped:error" });
|
|
1756
|
+
}, DESCRIPTION_REFRESH_TIMEOUT_MS);
|
|
1757
|
+
});
|
|
1758
|
+
try {
|
|
1759
|
+
return await Promise.race([this.refreshDescriptionWithoutPush(run, output, existingWorkspace), timedOut]);
|
|
1760
|
+
}
|
|
1761
|
+
finally {
|
|
1762
|
+
clearTimeout(timer);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* A fix round that changed nothing still re-derives its PR description (no
|
|
1767
|
+
* push). Never throws: any failure is recorded as skipped:error.
|
|
1768
|
+
*/
|
|
1769
|
+
async refreshDescriptionWithoutPush(run, output, existingWorkspace) {
|
|
1770
|
+
const vcs = this.options.vcs;
|
|
1771
|
+
if (!run.rootCodingRunId || !hasPullRequestDescription(run) || !vcs.refreshPullRequestDescription)
|
|
1772
|
+
return {};
|
|
1773
|
+
const state = await this.descriptionStateFor(run);
|
|
1774
|
+
if (!state || "failed" in state)
|
|
1775
|
+
return this.recordDescription(run, state, undefined);
|
|
1776
|
+
try {
|
|
1777
|
+
const input = this.preflightForCleanup(run);
|
|
1778
|
+
// Only the pull request is needed: a run another process launched has no workspace here.
|
|
1779
|
+
const workspace = existingWorkspace ??
|
|
1780
|
+
(input ? await vcs.recoverWorkspace(input).catch(() => null) : null) ??
|
|
1781
|
+
this.continuationTargetOf(run);
|
|
1782
|
+
if (!workspace)
|
|
1783
|
+
throw new Error("coding_workspace_lost");
|
|
1784
|
+
// The same in-flight access check a push gets: a revoked owner's run edits nothing.
|
|
1785
|
+
// Reloaded: the snapshot passed in predates the description reads above, and a link removed since then counts.
|
|
1786
|
+
await this.authorizeRepository(await this.requireCurrent(run.runId), { inFlight: true });
|
|
1787
|
+
const details = {
|
|
1788
|
+
summary: output.summary,
|
|
1789
|
+
tests: output.tests,
|
|
1790
|
+
tag: output.tag,
|
|
1791
|
+
...(await this.pullRequestRegistryReport(run)),
|
|
1792
|
+
...restoredDependenciesFor(output),
|
|
1793
|
+
...(await this.issueSourceFor(run)),
|
|
1794
|
+
description: { baseline: state.baseline },
|
|
1795
|
+
};
|
|
1796
|
+
const outcome = await vcs.refreshPullRequestDescription(workspace, details);
|
|
1797
|
+
return await this.recordDescription(run, state, outcome);
|
|
1798
|
+
}
|
|
1799
|
+
catch (err) {
|
|
1800
|
+
containerLog.warn({ err, runId: run.runId }, "could not refresh the pull request description");
|
|
1801
|
+
return { prDescriptionOutcome: "skipped:error" };
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
940
1804
|
/** Runs the onCodingRunTerminal hook; nothing it does, throws, or rejects reaches the caller. */
|
|
941
1805
|
async notifyTerminal(runId) {
|
|
942
1806
|
const hook = this.options.onCodingRunTerminal;
|
|
@@ -956,10 +1820,15 @@ export class ContainerExecutor {
|
|
|
956
1820
|
try {
|
|
957
1821
|
const report = await this.options.registryReport?.(runId);
|
|
958
1822
|
const packages = Array.isArray(report?.packages) ? report.packages : [];
|
|
959
|
-
|
|
1823
|
+
// A listing-only refusal is for get_run and the agent, not the PR (see LISTING_REFUSAL_FILENAME).
|
|
1824
|
+
const packageRefusals = Array.isArray(report?.packageRefusals) ? warningRefusals(report.packageRefusals) : [];
|
|
1825
|
+
const packageAudit = isPackageAuditMode(report?.packageAuditMode)
|
|
1826
|
+
? { mode: report.packageAuditMode, entries: Array.isArray(report.packageAudit) ? report.packageAudit : [] }
|
|
1827
|
+
: undefined;
|
|
960
1828
|
return {
|
|
961
1829
|
...(packages.length > 0 ? { packages } : {}),
|
|
962
1830
|
...(packageRefusals.length > 0 ? { packageRefusals } : {}),
|
|
1831
|
+
...(packageAudit ? { packageAudit } : {}),
|
|
963
1832
|
};
|
|
964
1833
|
}
|
|
965
1834
|
catch {
|
|
@@ -1081,12 +1950,89 @@ export class ContainerExecutor {
|
|
|
1081
1950
|
async warmUp() {
|
|
1082
1951
|
await this.options.jobs.warmUp?.();
|
|
1083
1952
|
}
|
|
1953
|
+
/**
|
|
1954
|
+
* Throws coding_run_no_longer_active once something else made the run terminal (a stop's
|
|
1955
|
+
* cancellation fence): nothing may then be pushed, opened, or rewritten on the host for it.
|
|
1956
|
+
*/
|
|
1957
|
+
requireActive(run) {
|
|
1958
|
+
if (TERMINAL_STATUSES.has(run.status))
|
|
1959
|
+
throw new Error("coding_run_no_longer_active");
|
|
1960
|
+
return run;
|
|
1961
|
+
}
|
|
1084
1962
|
async requireCurrent(runId) {
|
|
1085
1963
|
const current = await this.options.store.load(runId);
|
|
1086
1964
|
if (!current)
|
|
1087
1965
|
throw new Error("coding_run_not_found");
|
|
1088
1966
|
return current;
|
|
1089
1967
|
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Recovery's half of a capacity refusal: the launcher recorded it, but the process died before the
|
|
1970
|
+
* store put the run back in the queue. The planned handle the attempt persisted is the ownership
|
|
1971
|
+
* proof; the run waits in the queue again, never reported ended.
|
|
1972
|
+
*/
|
|
1973
|
+
async finishCapacityRequeue(run, planned) {
|
|
1974
|
+
const outcome = await this.options.store.requeueAfterCapacityRefusal(run.runId, { planned });
|
|
1975
|
+
if (outcome === "spend_recorded") {
|
|
1976
|
+
await this.abandon(run, "coding_ambiguous_provisioning");
|
|
1977
|
+
return { state: "lost", reason: "coding_ambiguous_provisioning" };
|
|
1978
|
+
}
|
|
1979
|
+
if (outcome === "not_owned") {
|
|
1980
|
+
const current = await this.options.store.load(run.runId);
|
|
1981
|
+
if (!current || !TERMINAL_STATUSES.has(current.status))
|
|
1982
|
+
return { state: "active" };
|
|
1983
|
+
await this.cleanupTerminal(current);
|
|
1984
|
+
return { state: "terminal" };
|
|
1985
|
+
}
|
|
1986
|
+
// The session went with the requeue; the rest of what the dead attempt left is removed here.
|
|
1987
|
+
this.options.capabilities.delete(run.runId);
|
|
1988
|
+
const input = this.preflightForCleanup(run);
|
|
1989
|
+
const workspace = input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null;
|
|
1990
|
+
if (workspace)
|
|
1991
|
+
await this.options.vcs.cleanup(workspace).catch(() => undefined);
|
|
1992
|
+
await rm(this.artifactPath(run.runId), { recursive: true, force: true }).catch(() => undefined);
|
|
1993
|
+
this.emit({ stage: "queued", runId: run.runId });
|
|
1994
|
+
containerLog.info({ runId: run.runId }, "coding run waiting for capacity: recovered a refusal its process left");
|
|
1995
|
+
return { state: "active" };
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* After drainCodingQueue failed a queued run (coding_queue_timeout): releases what an earlier
|
|
1999
|
+
* attempt the cluster refused left behind, as stop() does for a queued run. A refused
|
|
2000
|
+
* continuation already marked its pull request as being worked on; that check is finished
|
|
2001
|
+
* here. Best effort; the artifact is removed only where this process wrote it.
|
|
2002
|
+
*/
|
|
2003
|
+
async cleanupQueueTimeout(runId) {
|
|
2004
|
+
this.capacityRefusedAt.delete(runId);
|
|
2005
|
+
this.options.capabilities.delete(runId);
|
|
2006
|
+
const run = await this.options.store.load(runId);
|
|
2007
|
+
if (run) {
|
|
2008
|
+
const input = this.preflightForCleanup(run);
|
|
2009
|
+
const workspace = input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null;
|
|
2010
|
+
if (workspace)
|
|
2011
|
+
await this.options.vcs.cleanup(workspace).catch(() => undefined);
|
|
2012
|
+
const target = workspace ?? this.continuationTargetOf(run);
|
|
2013
|
+
if (target)
|
|
2014
|
+
await this.options.vcs.notifyContinuationFinished?.(target, "failed", { agentName: run.agentName });
|
|
2015
|
+
}
|
|
2016
|
+
await rm(this.artifactPath(runId), { recursive: true, force: true }).catch(() => undefined);
|
|
2017
|
+
}
|
|
2018
|
+
recordCapacityRefusal(runId) {
|
|
2019
|
+
const now = this.now().getTime();
|
|
2020
|
+
// Drop the ones whose backoff is over, so runs that never come back (timed out in the queue) do not pile up.
|
|
2021
|
+
for (const [id, at] of this.capacityRefusedAt) {
|
|
2022
|
+
if (now - at >= CAPACITY_RETRY_BACKOFF_MS)
|
|
2023
|
+
this.capacityRefusedAt.delete(id);
|
|
2024
|
+
}
|
|
2025
|
+
this.capacityRefusedAt.set(runId, now);
|
|
2026
|
+
}
|
|
2027
|
+
inCapacityBackoff(runId) {
|
|
2028
|
+
const at = this.capacityRefusedAt.get(runId);
|
|
2029
|
+
if (at === undefined)
|
|
2030
|
+
return false;
|
|
2031
|
+
if (this.now().getTime() - at < CAPACITY_RETRY_BACKOFF_MS)
|
|
2032
|
+
return true;
|
|
2033
|
+
this.capacityRefusedAt.delete(runId);
|
|
2034
|
+
return false;
|
|
2035
|
+
}
|
|
1090
2036
|
/**
|
|
1091
2037
|
* False when the launcher reports this run's job would not fit the cluster right now; the run
|
|
1092
2038
|
* is then queued and drainCodingQueue retries it. Launchers and stores without the hooks, and
|
|
@@ -1103,7 +2049,7 @@ export class ContainerExecutor {
|
|
|
1103
2049
|
this.emit({ stage: "queued", runId: run.runId });
|
|
1104
2050
|
return false;
|
|
1105
2051
|
}
|
|
1106
|
-
jobSpec(run, inputArtifact) {
|
|
2052
|
+
jobSpec(run, inputArtifact, options = {}) {
|
|
1107
2053
|
const provider = run.provider === "claude-code" ? "claude-code" : "codex";
|
|
1108
2054
|
if (provider === "claude-code" && (!run.workerImage || !this.options.claudeToolRunnerImage)) {
|
|
1109
2055
|
throw new Error("coding_provider_not_configured:claude-code");
|
|
@@ -1128,6 +2074,12 @@ export class ContainerExecutor {
|
|
|
1128
2074
|
// A run keeps the tool image it was dispatched with; rows from before CodingRun.toolImage use the default.
|
|
1129
2075
|
...(provider === "claude-code" ? { toolImage: run.toolImage ?? this.options.claudeToolRunnerImage } : {}),
|
|
1130
2076
|
inputArtifact,
|
|
2077
|
+
// Only when on: every other run's spec stays exactly what it was.
|
|
2078
|
+
...(run.dependencyCache ? { dependencyCache: true } : {}),
|
|
2079
|
+
// Claude Code only: the launcher gives the SDK's config dir, where the session grows, its own scratch.
|
|
2080
|
+
...(run.sessionResume && provider === "claude-code" ? { sessionResume: true } : {}),
|
|
2081
|
+
// Only for a run with MCP tools: the launcher then delivers WARDBY_MCP_TOKEN from the vault.
|
|
2082
|
+
...(options.mcpTools ? { mcpTools: true } : {}),
|
|
1131
2083
|
timeoutSec: run.timeoutSec,
|
|
1132
2084
|
limits: { ...this.options.limits, ...(run.workspaceDiskMb ? { diskMb: run.workspaceDiskMb } : {}) },
|
|
1133
2085
|
labels: {},
|
|
@@ -1154,8 +2106,13 @@ export class ContainerExecutor {
|
|
|
1154
2106
|
const destination = join(directory, "input.json");
|
|
1155
2107
|
const temporary = join(directory, `.input-${randomUUID()}.tmp`);
|
|
1156
2108
|
const services = parseStoredServices(run.services);
|
|
1157
|
-
const
|
|
1158
|
-
|
|
2109
|
+
const withCommit = withBaseCommit(run.task, workspace.baseCommit);
|
|
2110
|
+
const description = await this.currentDescriptionText(run);
|
|
2111
|
+
// The ticket first, then the description: each is cut to what's left of the task's budget.
|
|
2112
|
+
// The target-branch merge note (conflicts to resolve) comes before the ticket and the description.
|
|
2113
|
+
const withMerge = withTargetMerge(withCommit, workspace.targetMerge, run.headRef);
|
|
2114
|
+
const task = withPullRequestDescription(withLinkedIssue(withMerge, await this.linkedIssueFor(run, description)), description);
|
|
2115
|
+
if (withCommit === run.task && /^[0-9a-f]{40}$/.test(workspace.baseCommit)) {
|
|
1159
2116
|
containerLog.warn({ event: "coding.base_commit_omitted", runId: run.runId }, "the coding task left no room for the base commit line");
|
|
1160
2117
|
}
|
|
1161
2118
|
// A repository context read must never fail a run ("the run never fails because of context"):
|
|
@@ -1179,17 +2136,31 @@ export class ContainerExecutor {
|
|
|
1179
2136
|
headRef: run.headRef,
|
|
1180
2137
|
task,
|
|
1181
2138
|
model: run.model,
|
|
1182
|
-
|
|
2139
|
+
// The run's limit, not its grant: the proxy enforces the grant, and top-ups raise it while
|
|
2140
|
+
// the job runs. Claude Code's own budget stop (its SDK maxBudgetUsd) is set from this, so it
|
|
2141
|
+
// never stops a run before a top-up can; Codex has no stop of its own.
|
|
2142
|
+
budgetUsd: run.budgetLimitUsd ?? run.budgetUsd,
|
|
1183
2143
|
deadlineAt: deadlineAt.toISOString(),
|
|
1184
2144
|
continuationOf: run.rootCodingRunId ? { runId: run.rootCodingRunId } : undefined,
|
|
1185
2145
|
// Only when true: an untraced run's input stays exactly what older workers expect.
|
|
1186
2146
|
...(run.debugTrace ? { debugTrace: true } : {}),
|
|
2147
|
+
// Only on a traced run whose agent sets a cap: every other run's input stays exactly what older workers expect.
|
|
2148
|
+
...(run.debugTrace && run.debugTraceMaxMb ? { debugTraceMaxMb: run.debugTraceMaxMb } : {}),
|
|
1187
2149
|
// Only when the agent sets one: every other run's input stays exactly what older workers expect.
|
|
1188
2150
|
...(run.maxTurns ? { maxTurns: run.maxTurns } : {}),
|
|
2151
|
+
// Only when the agent sets one: every other run's input stays exactly what older workers expect.
|
|
2152
|
+
...(run.maxOutputTokens ? { maxOutputTokens: run.maxOutputTokens } : {}),
|
|
1189
2153
|
// Only when off: every other run's input stays exactly what older workers expect.
|
|
1190
2154
|
...(run.repoSkills === false ? { repoSkills: false } : {}),
|
|
1191
2155
|
// Only when off: every other run's input stays exactly what older workers expect.
|
|
1192
2156
|
...(run.claudeBareMode === false ? { claudeBareMode: false } : {}),
|
|
2157
|
+
// Only when on: every other run's input stays exactly what older workers expect. The volume
|
|
2158
|
+
// size (the job spec's limits.diskMb) bounds what the cache may restore.
|
|
2159
|
+
...(run.dependencyCache
|
|
2160
|
+
? { dependencyCache: true, diskMb: run.workspaceDiskMb ?? this.options.limits.diskMb }
|
|
2161
|
+
: {}),
|
|
2162
|
+
// Only when on: every other run's input stays exactly what older workers expect.
|
|
2163
|
+
...(run.sessionResume && run.provider === "claude-code" ? { sessionResume: true } : {}),
|
|
1193
2164
|
// Only when there are some: every other run's input stays exactly what older workers expect.
|
|
1194
2165
|
...(services.length > 0 ? { services: workerServices(services) } : {}),
|
|
1195
2166
|
// Only when there is something to load: every other run's input stays exactly what older workers expect.
|
|
@@ -1231,6 +2202,58 @@ export class ContainerExecutor {
|
|
|
1231
2202
|
artifactPath(runId) {
|
|
1232
2203
|
return resolve(this.artifactRoot, runId);
|
|
1233
2204
|
}
|
|
2205
|
+
/**
|
|
2206
|
+
* A continuation's pull request from the run record alone, for the host-side
|
|
2207
|
+
* finish signals when the workspace is gone (another control-plane process
|
|
2208
|
+
* launched the run). Null for a fresh run: it has no pull request yet.
|
|
2209
|
+
*/
|
|
2210
|
+
continuationTargetOf(run) {
|
|
2211
|
+
if (!run.rootCodingRunId)
|
|
2212
|
+
return null;
|
|
2213
|
+
return {
|
|
2214
|
+
runId: run.runId,
|
|
2215
|
+
repository: run.repository,
|
|
2216
|
+
baseRef: run.baseRef,
|
|
2217
|
+
headRef: run.headRef,
|
|
2218
|
+
continuation: { rootRunId: run.rootCodingRunId },
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
/**
|
|
2222
|
+
* Session resume: never throws, never changes the run's outcome. The recorder redacts; only a status
|
|
2223
|
+
* code is logged. The run capability is already gone from the vault, so it is not passed (the recorder
|
|
2224
|
+
* removes its fixed token shape).
|
|
2225
|
+
*/
|
|
2226
|
+
async recordTranscript(run, handle) {
|
|
2227
|
+
const recorder = this.options.transcripts;
|
|
2228
|
+
if (!recorder)
|
|
2229
|
+
return;
|
|
2230
|
+
const jobs = this.options.jobs;
|
|
2231
|
+
let timer;
|
|
2232
|
+
try {
|
|
2233
|
+
const recording = Promise.resolve()
|
|
2234
|
+
.then(() => recorder.record({
|
|
2235
|
+
runId: run.runId,
|
|
2236
|
+
agentId: run.agentId,
|
|
2237
|
+
provider: run.provider,
|
|
2238
|
+
read: (maxBytes) => jobs.readTranscript ? jobs.readTranscript(handle, maxBytes) : Promise.resolve(undefined),
|
|
2239
|
+
}))
|
|
2240
|
+
.catch(() => "failed");
|
|
2241
|
+
const timeout = new Promise((resolve) => {
|
|
2242
|
+
timer = setTimeout(() => resolve("timeout"), TRANSCRIPT_RECORD_TIMEOUT_MS);
|
|
2243
|
+
timer.unref();
|
|
2244
|
+
});
|
|
2245
|
+
const result = await Promise.race([recording, timeout]);
|
|
2246
|
+
if (result !== "stored")
|
|
2247
|
+
containerLog.info({ runId: run.runId, transcript: result }, "run transcript not stored");
|
|
2248
|
+
}
|
|
2249
|
+
catch {
|
|
2250
|
+
// never changes the run's outcome
|
|
2251
|
+
}
|
|
2252
|
+
finally {
|
|
2253
|
+
if (timer)
|
|
2254
|
+
clearTimeout(timer);
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
1234
2257
|
preflightForCleanup(run) {
|
|
1235
2258
|
if (!Array.isArray(run.protectedPaths) || !run.protectedPaths.every((path) => typeof path === "string"))
|
|
1236
2259
|
return null;
|
|
@@ -1255,6 +2278,7 @@ export class ContainerExecutor {
|
|
|
1255
2278
|
const failure = this.failure(reason);
|
|
1256
2279
|
await this.options.store.terminate(run.runId, "lost", failure.error, failure.audit);
|
|
1257
2280
|
this.terminal(run, "lost", failure.audit);
|
|
2281
|
+
await this.recordRefusal(run.runId, run.proxySessionId);
|
|
1258
2282
|
if (run.proxySessionId)
|
|
1259
2283
|
await this.options.sessions.cancelSession(run.proxySessionId).catch(() => undefined);
|
|
1260
2284
|
if (run.jobHandle) {
|
|
@@ -1264,14 +2288,17 @@ export class ContainerExecutor {
|
|
|
1264
2288
|
}
|
|
1265
2289
|
const input = this.preflightForCleanup(run);
|
|
1266
2290
|
const workspace = input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null;
|
|
1267
|
-
if (workspace)
|
|
2291
|
+
if (workspace)
|
|
1268
2292
|
await this.options.vcs.cleanup(workspace).catch(() => undefined);
|
|
1269
|
-
|
|
1270
|
-
|
|
2293
|
+
const target = workspace ?? this.continuationTargetOf(run);
|
|
2294
|
+
if (target)
|
|
2295
|
+
await this.options.vcs.notifyContinuationFinished?.(target, "failed", { agentName: run.agentName });
|
|
1271
2296
|
await rm(this.artifactPath(run.runId), { recursive: true, force: true }).catch(() => undefined);
|
|
1272
2297
|
this.emit({ stage: "cleanup", runId: run.runId, jobId: run.jobHandle?.id, cleanupSucceeded: true });
|
|
1273
2298
|
}
|
|
1274
2299
|
async cleanupTerminal(run) {
|
|
2300
|
+
// A run another path ended (or one recovered after a crash) still gets its refusal; the copy is idempotent.
|
|
2301
|
+
await this.recordRefusal(run.runId, run.proxySessionId);
|
|
1275
2302
|
if (run.proxySessionId)
|
|
1276
2303
|
await this.options.sessions.cancelSession(run.proxySessionId).catch(() => undefined);
|
|
1277
2304
|
if (run.jobHandle) {
|
|
@@ -1301,7 +2328,9 @@ export class ContainerExecutor {
|
|
|
1301
2328
|
const protectedPathFailed = run.status === "failed" && run.failureCategory === PROTECTED_PATH_CATEGORY;
|
|
1302
2329
|
await this.options.vcs.notifyContinuationFinished?.(workspace, outcome, {
|
|
1303
2330
|
agentName: run.agentName,
|
|
1304
|
-
...(outcome === "
|
|
2331
|
+
...(outcome === "succeeded" ? this.overrunDetails(run) : {}),
|
|
2332
|
+
...(outcome === "budget_exhausted" ? { budgetSentence: await this.budgetSentence(run) } : {}),
|
|
2333
|
+
...(outcome === "budget_exhausted" ? storedChangesPushed(run.result) : {}),
|
|
1305
2334
|
...(providerClass ? { providerSentence: providerSentence(providerClass) } : {}),
|
|
1306
2335
|
...(unreadyServices ? { serviceSentence: serviceUnreadySentence(unreadyServices) } : {}),
|
|
1307
2336
|
...(protectedPathFailed ? { protectedPathSentence: protectedPathSentence(null) } : {}),
|
|
@@ -1310,6 +2339,16 @@ export class ContainerExecutor {
|
|
|
1310
2339
|
await rm(this.artifactPath(run.runId), { recursive: true, force: true }).catch(() => undefined);
|
|
1311
2340
|
this.emit({ stage: "cleanup", runId: run.runId, jobId: run.jobHandle?.id, cleanupSucceeded: true });
|
|
1312
2341
|
}
|
|
2342
|
+
/**
|
|
2343
|
+
* Records the session's budget refusal on the run, at every ending: a run the proxy refused once may
|
|
2344
|
+
* still finish, fail for another reason, or be stopped. A session with no refusal adds nothing.
|
|
2345
|
+
* Best effort; never throws.
|
|
2346
|
+
*/
|
|
2347
|
+
async recordRefusal(runId, sessionId) {
|
|
2348
|
+
if (!sessionId)
|
|
2349
|
+
return;
|
|
2350
|
+
await this.options.store.recordBudgetRefusal?.(runId, sessionId).catch(() => undefined);
|
|
2351
|
+
}
|
|
1313
2352
|
/** Whether the proxy refused this run's session for budget. Never throws. */
|
|
1314
2353
|
async outOfBudget(sessionId) {
|
|
1315
2354
|
if (!sessionId || !this.options.sessions.budgetExhausted)
|
|
@@ -1337,11 +2376,23 @@ export class ContainerExecutor {
|
|
|
1337
2376
|
audit: { failureCategory: `provider_${provider.class}`, diagnosticId: logged.audit.diagnosticId },
|
|
1338
2377
|
};
|
|
1339
2378
|
}
|
|
2379
|
+
/** The overrun sentence for a finished run that spent past its base budget, as status details. */
|
|
2380
|
+
overrunDetails(run) {
|
|
2381
|
+
if (run.budgetBaseUsd === undefined)
|
|
2382
|
+
return {};
|
|
2383
|
+
const sentence = overrunSentence(run.costUsd, run.budgetBaseUsd, run.budgetLimitUsd ?? run.budgetUsd);
|
|
2384
|
+
return sentence ? { overrunSentence: sentence } : {};
|
|
2385
|
+
}
|
|
1340
2386
|
/** The host-safe sentence for a run that ran out of budget. */
|
|
1341
|
-
budgetSentence(run) {
|
|
2387
|
+
async budgetSentence(run) {
|
|
2388
|
+
const loaded = await this.options.store.budgetEvent?.(run.runId).catch(() => null);
|
|
2389
|
+
if (loaded)
|
|
2390
|
+
return hostBudgetSentence(loaded.event);
|
|
1342
2391
|
return budgetSentence({
|
|
1343
2392
|
runBudgetUsd: run.budgetUsd,
|
|
1344
2393
|
agentBudgetUsd: run.agentBudgetUsd ?? run.budgetUsd,
|
|
2394
|
+
baseBudgetUsd: run.budgetBaseUsd,
|
|
2395
|
+
limitUsd: run.budgetLimitUsd,
|
|
1345
2396
|
budgetGroupName: run.budgetGroupName,
|
|
1346
2397
|
});
|
|
1347
2398
|
}
|
|
@@ -1349,10 +2400,13 @@ export class ContainerExecutor {
|
|
|
1349
2400
|
* A failure the proxy's budget refusal caused. The failure itself is still
|
|
1350
2401
|
* logged under its diagnostic id, so the operator keeps the real reason.
|
|
1351
2402
|
*/
|
|
1352
|
-
budgetFailure(error, options = {}) {
|
|
2403
|
+
async budgetFailure(runId, error, options = {}) {
|
|
1353
2404
|
const logged = this.failure(error, options);
|
|
2405
|
+
const loaded = await this.options.store.budgetEvent?.(runId).catch(() => null);
|
|
1354
2406
|
return {
|
|
1355
|
-
error:
|
|
2407
|
+
error: loaded
|
|
2408
|
+
? `coding_budget_exhausted: ${ownerBudgetSentence(loaded.event, loaded.agentName)}`
|
|
2409
|
+
: "coding_budget_exhausted",
|
|
1356
2410
|
audit: { failureCategory: "budget", diagnosticId: logged.audit.diagnosticId },
|
|
1357
2411
|
};
|
|
1358
2412
|
}
|
|
@@ -1365,6 +2419,7 @@ export class ContainerExecutor {
|
|
|
1365
2419
|
}
|
|
1366
2420
|
}
|
|
1367
2421
|
terminal(run, outcome, audit) {
|
|
2422
|
+
this.capacityRefusedAt.delete(run.runId);
|
|
1368
2423
|
this.emit({
|
|
1369
2424
|
stage: "terminal",
|
|
1370
2425
|
runId: run.runId,
|
|
@@ -1426,6 +2481,10 @@ function safeError(error) {
|
|
|
1426
2481
|
const message = error instanceof Error ? error.message : "unknown";
|
|
1427
2482
|
return `coding_executor:${redactAndTruncate(message, 200).replace(/[^A-Za-z0-9_.:-]/g, "_")}`;
|
|
1428
2483
|
}
|
|
2484
|
+
/** The agent's MCP tool pins could not be read at launch (a database error): the run fails, nothing launched. */
|
|
2485
|
+
export const MCP_PINS_UNAVAILABLE_ERROR = "coding_mcp_pins_unavailable";
|
|
2486
|
+
/** Its category: the run's error is coding_failure_mcp_pins_unavailable:<diagnostic id>. */
|
|
2487
|
+
export const MCP_PINS_UNAVAILABLE_CATEGORY = "mcp_pins_unavailable";
|
|
1429
2488
|
/**
|
|
1430
2489
|
* Error-code prefixes that name their category outright, checked before the
|
|
1431
2490
|
* substring heuristics below. Those heuristics filed every `github_*` error
|
|
@@ -1449,13 +2508,21 @@ const CATEGORY_BY_PREFIX = [
|
|
|
1449
2508
|
// Checked before the generic "vcs_" fallback: the collected diff touched a
|
|
1450
2509
|
// path this agent may not edit, not a generic workspace/git failure.
|
|
1451
2510
|
["vcs_protected_path:", PROTECTED_PATH_CATEGORY],
|
|
2511
|
+
// A continuation's target-branch merge (git.ts): conflict markers left in a
|
|
2512
|
+
// conflicted file, or a target branch that could not be fetched or merged.
|
|
2513
|
+
[CONFLICT_MARKERS_ERROR_PREFIX, MERGE_CONFLICT_CATEGORY],
|
|
2514
|
+
[TARGET_MERGE_FAILED_ERROR, TARGET_MERGE_CATEGORY],
|
|
1452
2515
|
// A continuation whose pull request was merged or closed: nothing pushed (coding/continuation-wording.ts).
|
|
1453
2516
|
[CONTINUATION_CLOSED_ERROR, CONTINUATION_CLOSED_CATEGORY],
|
|
1454
2517
|
// A provider whose worker images this deployment doesn't configure (codex or claude-code): configuration.
|
|
1455
2518
|
["coding_provider_not_configured", "preflight"],
|
|
1456
2519
|
// A GitHub repository on a server with no GitHub App: configuration, like an unsupported service.
|
|
1457
2520
|
["vcs_github_not_configured", "preflight"],
|
|
2521
|
+
// The same for any other host: a repository whose provider this server has not registered.
|
|
2522
|
+
["vcs_host_provider_not_configured", "preflight"],
|
|
1458
2523
|
["vcs_", "workspace"],
|
|
2524
|
+
// The agent's MCP tool pins could not be read at launch: nothing was launched.
|
|
2525
|
+
[MCP_PINS_UNAVAILABLE_ERROR, MCP_PINS_UNAVAILABLE_CATEGORY],
|
|
1459
2526
|
// Local repositories (LocalRepoError codes): the same family as the vcs_ errors.
|
|
1460
2527
|
["local_", "workspace"],
|
|
1461
2528
|
["git_", "workspace"],
|
|
@@ -1507,6 +2574,53 @@ function failureCategory(error) {
|
|
|
1507
2574
|
return "cancelled";
|
|
1508
2575
|
return "executor";
|
|
1509
2576
|
}
|
|
2577
|
+
/** A stored CodingRun result's summary (the agent-written description text), when it has one. */
|
|
2578
|
+
function storedSummary(result) {
|
|
2579
|
+
const summary = storedField(result, "summary");
|
|
2580
|
+
return typeof summary === "string" ? summary : undefined;
|
|
2581
|
+
}
|
|
2582
|
+
/** One field of a stored JSON object; undefined when it isn't an object. */
|
|
2583
|
+
function storedField(value, key) {
|
|
2584
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
2585
|
+
return undefined;
|
|
2586
|
+
return value[key];
|
|
2587
|
+
}
|
|
2588
|
+
/** Stored test results the PR body can list (command + outcome); anything else is dropped. */
|
|
2589
|
+
function storedTests(value) {
|
|
2590
|
+
if (!Array.isArray(value))
|
|
2591
|
+
return [];
|
|
2592
|
+
return value.flatMap((test) => {
|
|
2593
|
+
const command = storedField(test, "command");
|
|
2594
|
+
const outcome = storedField(test, "outcome");
|
|
2595
|
+
return typeof command === "string" && (outcome === "passed" || outcome === "failed" || outcome === "skipped")
|
|
2596
|
+
? [{ command, outcome }]
|
|
2597
|
+
: [];
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2600
|
+
/**
|
|
2601
|
+
* Packages from several runs' reports, in order: one entry per ecosystem +
|
|
2602
|
+
* name + version, first appearance first, accepted advisories merged.
|
|
2603
|
+
*/
|
|
2604
|
+
function mergePackages(lists) {
|
|
2605
|
+
const merged = new Map();
|
|
2606
|
+
for (const list of lists) {
|
|
2607
|
+
for (const pkg of list) {
|
|
2608
|
+
const key = `${pkg.ecosystem}\0${pkg.name}\0${pkg.version}`;
|
|
2609
|
+
const existing = merged.get(key);
|
|
2610
|
+
if (!existing) {
|
|
2611
|
+
merged.set(key, {
|
|
2612
|
+
...pkg,
|
|
2613
|
+
...(pkg.acceptedAdvisories ? { acceptedAdvisories: [...pkg.acceptedAdvisories] } : {}),
|
|
2614
|
+
});
|
|
2615
|
+
continue;
|
|
2616
|
+
}
|
|
2617
|
+
if (pkg.acceptedAdvisories?.length) {
|
|
2618
|
+
existing.acceptedAdvisories = [...new Set([...(existing.acceptedAdvisories ?? []), ...pkg.acceptedAdvisories])];
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
return [...merged.values()];
|
|
2623
|
+
}
|
|
1510
2624
|
function stableJson(value) {
|
|
1511
2625
|
if (Array.isArray(value))
|
|
1512
2626
|
return `[${value.map(stableJson).join(",")}]`;
|