@citeark/agent 0.3.20
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/LICENSE +202 -0
- package/README.md +128 -0
- package/data/dataset-source-registry.v1.json +300 -0
- package/dist/arkgraph/boot.js +6 -0
- package/dist/arkgraph/index.html +1 -0
- package/dist/arkgraph/viewer.css +1 -0
- package/dist/arkgraph/viewer.en.css +1 -0
- package/dist/arkgraph/viewer.en.js +49 -0
- package/dist/arkgraph/viewer.en.js.LEGAL.txt +56 -0
- package/dist/arkgraph/viewer.js +49 -0
- package/dist/arkgraph/viewer.js.LEGAL.txt +56 -0
- package/docker/claude-code/Dockerfile +97 -0
- package/docker/claude-code/codex-pro-relay.mjs +466 -0
- package/docker/claude-code/runtime-contract-check.mjs +79 -0
- package/docs/arkgraph-reading.md +79 -0
- package/docs/configuration.md +100 -0
- package/docs/integration.md +92 -0
- package/docs/maturity-plan.md +27 -0
- package/docs/npm-release.md +44 -0
- package/docs/paper-reading.md +40 -0
- package/docs/research-plan-granularity.md +27 -0
- package/docs/terminal.md +49 -0
- package/examples/toy-evaluation/compile-task.json +27 -0
- package/examples/toy-evaluation/paper.md +5 -0
- package/examples/toy-evaluation/repository/README.md +9 -0
- package/examples/toy-evaluation/repository/checkpoint.json +4 -0
- package/examples/toy-evaluation/repository/evaluate.py +17 -0
- package/examples/toy-evaluation/task.json +81 -0
- package/package.json +59 -0
- package/prompts/compile-research.md +58 -0
- package/prompts/execute-contract.md +72 -0
- package/prompts/execute-workspace-simple.md +51 -0
- package/prompts/execute-workspace.md +34 -0
- package/prompts/prepare-reproduction.md +82 -0
- package/prompts/repair-research.md +45 -0
- package/protocol/CAP.md +129 -0
- package/protocol/LICENSE +12 -0
- package/protocol/MAPPINGS.md +72 -0
- package/protocol/README.md +38 -0
- package/protocol/conformance-v2.0-alpha.1.json +36 -0
- package/protocol/examples/arkgraph/checkpoint-evaluation.json +309 -0
- package/protocol/examples/arkgraph/fixtures.mjs +49 -0
- package/protocol/examples/arkgraph/paper-free.json +291 -0
- package/protocol/examples/arkgraph/partial-failure.json +344 -0
- package/protocol/examples/arkgraph/training-evaluation.json +443 -0
- package/protocol/profiles/agent-trace.md +16 -0
- package/protocol/profiles/computational-run.md +16 -0
- package/protocol/profiles/core.md +15 -0
- package/protocol/profiles/public-bundle.md +18 -0
- package/protocol/profiles/reproduction.md +29 -0
- package/protocol/profiles/research-compilation.md +44 -0
- package/protocol/profiles/research-plan.md +39 -0
- package/protocol/profiles/restricted-evidence.md +15 -0
- package/runtime/bootstrap-autodl-runtime.sh +314 -0
- package/runtime/create-runtime-venv.sh +41 -0
- package/runtime/install-local-cpu-runtime.sh +23 -0
- package/runtime/install-scientific-runtime.sh +153 -0
- package/runtime/mineru/parse.py +62 -0
- package/runtime/mineru/requirements.txt +4 -0
- package/runtime/requirements-baseline.txt +38 -0
- package/schemas/cap/v2/activity.schema.json +47 -0
- package/schemas/cap/v2/agent.schema.json +32 -0
- package/schemas/cap/v2/assertion.schema.json +110 -0
- package/schemas/cap/v2/descriptor.schema.json +243 -0
- package/schemas/cap/v2/entity.schema.json +64 -0
- package/schemas/cap/v2/manifest.schema.json +67 -0
- package/schemas/cap/v2/relation.schema.json +82 -0
- package/schemas/compute-catalog.schema.json +63 -0
- package/schemas/compute-decision.schema.json +27 -0
- package/schemas/execution-contract.schema.json +1024 -0
- package/schemas/research-card.schema.json +30 -0
- package/schemas/research-inventory-draft.schema.json +366 -0
- package/schemas/research.schema.json +1044 -0
- package/schemas/result.schema.json +173 -0
- package/schemas/verification-policy.schema.json +47 -0
- package/schemas/verified-conclusion.schema.json +58 -0
- package/schemas/workspace-summary.schema.json +24 -0
- package/scripts/build-arkgraph-view.mjs +12 -0
- package/scripts/check-execution-feasibility.mjs +24 -0
- package/scripts/check-syntax.mjs +15 -0
- package/scripts/deterministic-asset-preparation.py +438 -0
- package/scripts/package-cap.mjs +23 -0
- package/scripts/package-local-agent.mjs +23 -0
- package/scripts/preview-arkgraph.mjs +25 -0
- package/scripts/replay-research-compiler-candidate.mjs +134 -0
- package/scripts/review-compiler-sources.mjs +44 -0
- package/scripts/run-asset-preparation.sh +17 -0
- package/scripts/run-research-plan.mjs +98 -0
- package/scripts/validate-asset-preparation.py +290 -0
- package/scripts/verify-local-runtime.mjs +57 -0
- package/scripts/verify-npm-package.mjs +57 -0
- package/src/adapters/paper2agent.mjs +107 -0
- package/src/assets/cache.mjs +159 -0
- package/src/assets/compute.mjs +98 -0
- package/src/assets/executor.mjs +145 -0
- package/src/assets/lifecycle.mjs +213 -0
- package/src/assets/manifest.mjs +242 -0
- package/src/assets/opportunistic-preparation.mjs +81 -0
- package/src/assets/plan.mjs +411 -0
- package/src/assets/prompts.mjs +29 -0
- package/src/assets/public-asset-probe.mjs +525 -0
- package/src/assets/qualification.mjs +119 -0
- package/src/assets/readiness.mjs +130 -0
- package/src/assets/reproduction-admission.mjs +355 -0
- package/src/assets/requirements.mjs +152 -0
- package/src/assets/source-grounding.mjs +341 -0
- package/src/assets/source-policy.mjs +118 -0
- package/src/autodl/client.mjs +260 -0
- package/src/autodl/ssh.mjs +380 -0
- package/src/autodl/tools.mjs +129 -0
- package/src/cap/redaction.mjs +38 -0
- package/src/cap/v2/archive.mjs +152 -0
- package/src/cap/v2/attestation.mjs +204 -0
- package/src/cap/v2/canonical-json.mjs +114 -0
- package/src/cap/v2/compilation-artifact.mjs +240 -0
- package/src/cap/v2/core.mjs +282 -0
- package/src/cap/v2/measurement-assessment-records.mjs +23 -0
- package/src/cap/v2/pipeline-artifact.mjs +922 -0
- package/src/cap/v2/read.mjs +41 -0
- package/src/cap/v2/reassessment-artifact.mjs +383 -0
- package/src/cap/v2/research-artifact.mjs +231 -0
- package/src/cap/v2/research-map-records.mjs +46 -0
- package/src/cap/v2/research-object-records.mjs +163 -0
- package/src/cap/v2/research-records.mjs +187 -0
- package/src/cap/v2/verify.mjs +642 -0
- package/src/cli.mjs +1146 -0
- package/src/compute/autodl-pro-compiler.mjs +347 -0
- package/src/compute/autodl-pro-executor.mjs +459 -0
- package/src/compute/autodl-pro-job.mjs +843 -0
- package/src/compute/autodl-pro-network.mjs +295 -0
- package/src/compute/autodl-pro-remote.mjs +810 -0
- package/src/compute/autodl-pro-staging.mjs +117 -0
- package/src/compute/campaign.mjs +110 -0
- package/src/compute/catalog.mjs +123 -0
- package/src/compute/checkpoint-protocol.mjs +154 -0
- package/src/compute/codex-account-lock.mjs +111 -0
- package/src/compute/codex-account-session.mjs +107 -0
- package/src/compute/compiler-profile.mjs +38 -0
- package/src/compute/compiler-router.mjs +23 -0
- package/src/compute/coordinator-recovery.mjs +210 -0
- package/src/compute/executor-router.mjs +29 -0
- package/src/compute/gcp-batch-compiler.mjs +685 -0
- package/src/compute/gcp-batch-executor.mjs +1215 -0
- package/src/compute/gcp-batch-failure.mjs +92 -0
- package/src/compute/gcp-batch-job.mjs +527 -0
- package/src/compute/gcp-batch-lifecycle.mjs +81 -0
- package/src/compute/gcp-checkpoint-worker.mjs +1633 -0
- package/src/compute/local-codex-compiler.mjs +52 -0
- package/src/compute/measurement-hardware.mjs +128 -0
- package/src/compute/remote-attempt.mjs +226 -0
- package/src/compute/requirements.mjs +124 -0
- package/src/compute/research-phases.mjs +48 -0
- package/src/compute/scheduler.mjs +452 -0
- package/src/compute/shared-workloads.mjs +26 -0
- package/src/compute/stage-archive.mjs +79 -0
- package/src/contracts/campaign-contract.mjs +52 -0
- package/src/contracts/execution-contract.mjs +819 -0
- package/src/contracts/execution-mode.mjs +19 -0
- package/src/contracts/execution-timeouts.mjs +45 -0
- package/src/contracts/execution-workload.mjs +68 -0
- package/src/contracts/preflight-schema.mjs +25 -0
- package/src/contracts/public-contract.mjs +63 -0
- package/src/contracts/subject-tags.mjs +31 -0
- package/src/dashboard/data.mjs +898 -0
- package/src/dashboard/server.mjs +79 -0
- package/src/dashboard/static/dashboard.css +366 -0
- package/src/dashboard/static/dashboard.js +560 -0
- package/src/dashboard/static/index.html +85 -0
- package/src/deployment/community-policy.mjs +9 -0
- package/src/deployment/environment.mjs +112 -0
- package/src/deployment/guided.mjs +98 -0
- package/src/deployment/handoff.mjs +102 -0
- package/src/deployment/local-contract.mjs +31 -0
- package/src/deployment/local.mjs +100 -0
- package/src/deployment/prepare.mjs +46 -0
- package/src/deployment/recipe.mjs +108 -0
- package/src/deployment/supplement.mjs +51 -0
- package/src/deployment/terminal.mjs +43 -0
- package/src/diagnosis/renderer.mjs +75 -0
- package/src/diagnosis/target-failure.mjs +46 -0
- package/src/evidence/parser-registry.mjs +54 -0
- package/src/evidence/parsers/fasttext-classification.mjs +82 -0
- package/src/evidence/parsers/json-scalar.mjs +96 -0
- package/src/evidence/parsers/simcse-senteval.mjs +104 -0
- package/src/evidence/parsers/starspace-classification.mjs +78 -0
- package/src/evidence/registry.mjs +147 -0
- package/src/execution/runner-audit.mjs +473 -0
- package/src/gcp/auth.mjs +106 -0
- package/src/gcp/batch-client.mjs +120 -0
- package/src/gcp/resource-discovery.mjs +177 -0
- package/src/gcp/rest.mjs +82 -0
- package/src/gcp/secret-manager.mjs +34 -0
- package/src/gcp/signed-url.mjs +133 -0
- package/src/gcp/storage.mjs +220 -0
- package/src/graph/command.mjs +41 -0
- package/src/graph/execution.mjs +97 -0
- package/src/graph/model.mjs +37 -0
- package/src/graph/presentation.mjs +110 -0
- package/src/graph/query.mjs +159 -0
- package/src/graph/research-relations.mjs +69 -0
- package/src/graph/source-page.mjs +12 -0
- package/src/graph/source-preview.mjs +34 -0
- package/src/graph/validate.mjs +76 -0
- package/src/job.mjs +496 -0
- package/src/network/autodl-routing-proxy.mjs +462 -0
- package/src/network/egress-proxy.mjs +158 -0
- package/src/observability/event-contract.mjs +230 -0
- package/src/observability/pipeline-monitor.mjs +166 -0
- package/src/pipeline/orchestrator.mjs +1281 -0
- package/src/pipeline/recovery-error.mjs +11 -0
- package/src/pipeline/replay.mjs +304 -0
- package/src/pipeline/shared-execution.mjs +115 -0
- package/src/pipeline/stage-checkpoint.mjs +86 -0
- package/src/pipeline/stage-recovery.mjs +101 -0
- package/src/pipeline/targets.mjs +110 -0
- package/src/process.mjs +143 -0
- package/src/protocol.mjs +312 -0
- package/src/provider/codex-account.mjs +44 -0
- package/src/provider/codex-completion.mjs +49 -0
- package/src/provider/completion.mjs +292 -0
- package/src/provider/model-client.mjs +44 -0
- package/src/provider/model-route.mjs +29 -0
- package/src/provider/openrouter-readiness.mjs +189 -0
- package/src/provider/reader-bridge.mjs +35 -0
- package/src/provider/relay.mjs +263 -0
- package/src/provider/runtime-auth.mjs +40 -0
- package/src/public/cap.d.mts +90 -0
- package/src/public/cap.mjs +12 -0
- package/src/public/contracts.d.mts +2 -0
- package/src/public/host.mjs +171 -0
- package/src/public/operations.d.mts +11 -0
- package/src/public/presentation.d.mts +4 -0
- package/src/records/views.mjs +26 -0
- package/src/remote/command.mjs +178 -0
- package/src/remote/ssh.mjs +59 -0
- package/src/repository-origin.mjs +81 -0
- package/src/reproduction/evidence-feedback.mjs +96 -0
- package/src/reproduction/incomplete-initialization.mjs +25 -0
- package/src/reproduction/lifecycle.mjs +253 -0
- package/src/reproduction/plan.mjs +132 -0
- package/src/reproduction/prompts.mjs +70 -0
- package/src/reproduction/runner.mjs +188 -0
- package/src/reproduction/summary.mjs +130 -0
- package/src/reproduction/workspace-mode.mjs +7 -0
- package/src/research/automatic-admission.mjs +156 -0
- package/src/research/compiler-coverage.mjs +85 -0
- package/src/research/compiler-failure.mjs +24 -0
- package/src/research/compiler-normalization-guards.mjs +112 -0
- package/src/research/compiler-repair.mjs +3 -0
- package/src/research/compiler.mjs +853 -0
- package/src/research/continuation-selection.mjs +26 -0
- package/src/research/execution-graph-context.mjs +43 -0
- package/src/research/experiment-importance.mjs +15 -0
- package/src/research/inventory-handoff.mjs +104 -0
- package/src/research/inventory-revisions.mjs +32 -0
- package/src/research/mineru-local.mjs +73 -0
- package/src/research/paper-command.mjs +19 -0
- package/src/research/paper-markdown.mjs +180 -0
- package/src/research/paper-source-map.mjs +69 -0
- package/src/research/planning-policy.mjs +88 -0
- package/src/research/reference-materials.mjs +11 -0
- package/src/research/reproduction-scope.mjs +30 -0
- package/src/research/research-map.mjs +94 -0
- package/src/research/research-objects.mjs +88 -0
- package/src/research/source-discovery.mjs +646 -0
- package/src/research/source-observations.mjs +75 -0
- package/src/research/source-review-cli-mcp.mjs +26 -0
- package/src/research/source-review-input.mjs +209 -0
- package/src/research/source-review-local-codex.mjs +36 -0
- package/src/research/source-review-model.mjs +70 -0
- package/src/research/source-review.mjs +173 -0
- package/src/research/structure.mjs +3163 -0
- package/src/research-card/renderer.mjs +277 -0
- package/src/research-card/verified-conclusion.mjs +143 -0
- package/src/results/output-registry.mjs +183 -0
- package/src/runtime/claude-code.mjs +52 -0
- package/src/runtime/codex-capacity-retry.mjs +87 -0
- package/src/runtime/codex.mjs +64 -0
- package/src/runtime/config.mjs +157 -0
- package/src/runtime/final-output.mjs +40 -0
- package/src/runtime/index.mjs +21 -0
- package/src/runtime/local-codex.mjs +74 -0
- package/src/runtime/opencode.mjs +95 -0
- package/src/runtime/prompt.mjs +13 -0
- package/src/sandbox/docker.mjs +363 -0
- package/src/settings/command.mjs +297 -0
- package/src/settings/store.mjs +119 -0
- package/src/telemetry/pricing.mjs +68 -0
- package/src/telemetry/usage.mjs +265 -0
- package/src/terminal/events.mjs +97 -0
- package/src/terminal/input.mjs +40 -0
- package/src/terminal/plain.mjs +40 -0
- package/src/terminal/remote-stream.mjs +22 -0
- package/src/terminal/screen.mjs +214 -0
- package/src/terminal/transcript.mjs +69 -0
- package/src/util.mjs +107 -0
- package/src/verification/ai-assessor.mjs +534 -0
- package/src/verification/claim-evaluator.mjs +242 -0
- package/src/verification/evidence-context.mjs +165 -0
- package/src/verification/evidence-reader.mjs +95 -0
- package/src/verification/integrity.mjs +570 -0
- package/src/verification/tolerance.mjs +32 -0
- package/src/workloads/cpu-research-preparation.mjs +56 -0
- package/src/workloads/definition.mjs +74 -0
- package/src/workloads/phase-aware-reproduction.mjs +46 -0
- package/src/workloads/reproduction.mjs +85 -0
- package/src/workspace/command.mjs +242 -0
- package/src/workspace/control.mjs +49 -0
- package/src/workspace/entry.mjs +28 -0
- package/src/workspace/input.mjs +93 -0
- package/src/workspace/interactive.mjs +94 -0
- package/src/workspace/jobs.mjs +418 -0
- package/src/workspace/session.mjs +97 -0
- package/src/workspace/worker.mjs +137 -0
- package/ui/arkgraph/ambient-motion.mjs +10 -0
- package/ui/arkgraph/app.jsx +153 -0
- package/ui/arkgraph/boot.js +6 -0
- package/ui/arkgraph/camera-motion.mjs +20 -0
- package/ui/arkgraph/context-reveal.mjs +39 -0
- package/ui/arkgraph/details.css +3 -0
- package/ui/arkgraph/entry.jsx +28 -0
- package/ui/arkgraph/experiment-curves.mjs +17 -0
- package/ui/arkgraph/experiment-selection.mjs +15 -0
- package/ui/arkgraph/experiment-style.css +26 -0
- package/ui/arkgraph/experiment-ui.jsx +32 -0
- package/ui/arkgraph/frame.html +1 -0
- package/ui/arkgraph/graph-gestures.mjs +62 -0
- package/ui/arkgraph/label-layout.mjs +57 -0
- package/ui/arkgraph/locales/en.json +229 -0
- package/ui/arkgraph/locales/source-types.json +15 -0
- package/ui/arkgraph/localization-build.mjs +27 -0
- package/ui/arkgraph/material-build.mjs +23 -0
- package/ui/arkgraph/material-colors.mjs +39 -0
- package/ui/arkgraph/material-style.css +15 -0
- package/ui/arkgraph/open-graph.jsx +326 -0
- package/ui/arkgraph/outline.jsx +49 -0
- package/ui/arkgraph/package-lock.json +888 -0
- package/ui/arkgraph/package.json +17 -0
- package/ui/arkgraph/reading-layout.mjs +130 -0
- package/ui/arkgraph/reading-presentation.mjs +73 -0
- package/ui/arkgraph/record-detail.css +51 -0
- package/ui/arkgraph/record-details.jsx +29 -0
- package/ui/arkgraph/research-types.mjs +31 -0
- package/ui/arkgraph/selection-mark.jsx +6 -0
- package/ui/arkgraph/soft-spine.mjs +26 -0
- package/ui/arkgraph/steering-style.css +187 -0
- package/ui/arkgraph/style.css +272 -0
|
@@ -0,0 +1,1215 @@
|
|
|
1
|
+
import { hasSessionEvents } from '../terminal/events.mjs';
|
|
2
|
+
import { lstat, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { saveRunRecovery, saveRemoteRecovery, restoreRemoteRecovery } from "./coordinator-recovery.mjs";
|
|
7
|
+
import { GcpBatchClient } from "../gcp/batch-client.mjs";
|
|
8
|
+
import { GoogleAccessTokenProvider } from "../gcp/auth.mjs";
|
|
9
|
+
import { GcsStagingClient } from "../gcp/storage.mjs";
|
|
10
|
+
import { updateRunState } from "../job.mjs";
|
|
11
|
+
import { captureProcess } from "../process.mjs";
|
|
12
|
+
import { createRemoteStageArchive } from "./stage-archive.mjs";
|
|
13
|
+
import { agentInvocationPromptArgument, buildAgentInvocation } from "../runtime/index.mjs";
|
|
14
|
+
import {
|
|
15
|
+
CODEX_FINAL_MESSAGE_PATH,
|
|
16
|
+
recoverJsonFromFinalMessage,
|
|
17
|
+
} from "../runtime/final-output.mjs";
|
|
18
|
+
import {
|
|
19
|
+
CiteArkError,
|
|
20
|
+
pathExists,
|
|
21
|
+
printableCommand,
|
|
22
|
+
readJson,
|
|
23
|
+
sha256File,
|
|
24
|
+
sha256Value,
|
|
25
|
+
writeJson,
|
|
26
|
+
} from "../util.mjs";
|
|
27
|
+
import {
|
|
28
|
+
buildGcpBatchJob,
|
|
29
|
+
remoteProviderSession,
|
|
30
|
+
resolveCheckpointObjects,
|
|
31
|
+
resolveGcpBatchConfig,
|
|
32
|
+
} from "./gcp-batch-job.mjs";
|
|
33
|
+
import { assertSuccessfulGcpBatchTerminal } from "./gcp-batch-failure.mjs";
|
|
34
|
+
import {
|
|
35
|
+
createLockedBatchJob,
|
|
36
|
+
deleteBatchJob,
|
|
37
|
+
releaseBatchJobAccountLock,
|
|
38
|
+
} from "./gcp-batch-lifecycle.mjs";
|
|
39
|
+
import {
|
|
40
|
+
infrastructureResumePrompt,
|
|
41
|
+
summarizeInfrastructureAttempt,
|
|
42
|
+
targetRecoveryPrompt,
|
|
43
|
+
} from "./checkpoint-protocol.mjs";
|
|
44
|
+
import {
|
|
45
|
+
describeRemoteAgentFailure,
|
|
46
|
+
readRemoteAttempt,
|
|
47
|
+
reconcileRemoteAgentOutcome,
|
|
48
|
+
} from "./remote-attempt.mjs";
|
|
49
|
+
import { prepareReproduction } from "../reproduction/lifecycle.mjs";
|
|
50
|
+
import { agentWorkloadMetadata, defineAgentWorkload } from "../workloads/definition.mjs";
|
|
51
|
+
import { createAutonomousReproductionWorkload } from "../workloads/reproduction.mjs";
|
|
52
|
+
|
|
53
|
+
export async function executeGcpBatchReproduction({
|
|
54
|
+
taskPath,
|
|
55
|
+
referenceMaterialsPath,
|
|
56
|
+
runsDirectory,
|
|
57
|
+
imageOverride,
|
|
58
|
+
repositoryOverride,
|
|
59
|
+
paperOverride,
|
|
60
|
+
computeDecision,
|
|
61
|
+
runId,
|
|
62
|
+
agentRuntime,
|
|
63
|
+
executorOptions = {},
|
|
64
|
+
workloadAdapter = null,
|
|
65
|
+
dryRun = false,
|
|
66
|
+
progress = () => {},
|
|
67
|
+
}) {
|
|
68
|
+
const workload = defineAgentWorkload(
|
|
69
|
+
workloadAdapter ?? createAutonomousReproductionWorkload(),
|
|
70
|
+
);
|
|
71
|
+
const gcpOptions = executorOptions.gcpBatch ?? {};
|
|
72
|
+
const bundle = await prepareReproduction({
|
|
73
|
+
taskPath,
|
|
74
|
+
referenceMaterialsPath,
|
|
75
|
+
runsDirectory,
|
|
76
|
+
imageOverride,
|
|
77
|
+
repositoryOverride,
|
|
78
|
+
paperOverride,
|
|
79
|
+
computeDecision,
|
|
80
|
+
runId,
|
|
81
|
+
agentRuntime,
|
|
82
|
+
requireApi: false,
|
|
83
|
+
sandboxProvider: "gcp-batch",
|
|
84
|
+
validateScientificComputeDecision: workload.validateScientificComputeDecision,
|
|
85
|
+
});
|
|
86
|
+
if (bundle.restored && bundle.state.recoveryRemoteResult && !dryRun) {
|
|
87
|
+
const attempt = bundle.state.recoveryRemoteResult.attempt;
|
|
88
|
+
const completionPath = path.join(bundle.directories.run, workload.completionRelativePath);
|
|
89
|
+
if (workload.recoverFinalMessage !== false) await recoverJsonFromFinalMessage({
|
|
90
|
+
finalMessagePath: hostRuntimePath(bundle, CODEX_FINAL_MESSAGE_PATH), outputPath: completionPath });
|
|
91
|
+
const submitted = await workload.validate({ bundle, completionPath });
|
|
92
|
+
const remoteFailure = await describeRemoteAgentFailure(bundle, attempt);
|
|
93
|
+
const reconciled = reconcileRemoteAgentOutcome({ validation: submitted, remoteFailure });
|
|
94
|
+
if (reconciled.blockingFailure || reconciled.validation.issues.length) {
|
|
95
|
+
throw new CiteArkError(`恢复的结果仍需修复:${reconciled.blockingFailure?.message ?? reconciled.validation.issues.join(";")}`, {
|
|
96
|
+
failureCode: "recovery.result_validation", failureCategory: "protocol", retryable: false,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
progress("复用已下载的远端结果,仅继续协调器验证,不创建 Batch");
|
|
100
|
+
return workload.finalize({ bundle, validation: reconciled.validation, attempts: recoveryAttempts(bundle),
|
|
101
|
+
timedOut: false, sandboxImageIdentity: bundle.state.sandboxImageIdentity ?? remoteImageIdentity(bundle.executionEnvironment.image),
|
|
102
|
+
cloudExecution: bundle.state.cloudExecution, checkpoint: bundle.state.durableRemoteCheckpoint });
|
|
103
|
+
}
|
|
104
|
+
await workload.prepare?.(bundle);
|
|
105
|
+
bundle.paperBudget = gcpOptions.paperBudget;
|
|
106
|
+
if (bundle.paperBudget) bundle.runtimeAgent = { ...bundle.runtimeAgent, paperBudgetEnforced: true };
|
|
107
|
+
await updateRunState(bundle, agentWorkloadMetadata(workload));
|
|
108
|
+
const config = resolveGcpBatchConfig({ bundle, options: gcpOptions });
|
|
109
|
+
const clients = createClients(config, gcpOptions.clients);
|
|
110
|
+
if (bundle.restored && !dryRun) {
|
|
111
|
+
const recovered = bundle.state.recoveryContinueAgent ? null : await resumeExistingRemoteResult({ bundle, workload, config, clients, gcpOptions, progress });
|
|
112
|
+
if (recovered) return recovered;
|
|
113
|
+
const restoredCheckpoint = await restoreRemoteRecovery({ bundle, storage: clients.storage, bucket: config.stagingBucket });
|
|
114
|
+
if (restoredCheckpoint) {
|
|
115
|
+
config.restoreCheckpointPrefix = restoredCheckpoint.checkpointPrefix;
|
|
116
|
+
config.restoreCheckpointBucket = restoredCheckpoint.bucket;
|
|
117
|
+
}
|
|
118
|
+
if (bundle.state.recoveryContinueAgent) config.reuseCompletedResultOnRestore = false;
|
|
119
|
+
bundle.state.recoveryGeneration = (bundle.state.recoveryGeneration ?? 0) + 1;
|
|
120
|
+
config.stagingPrefix += `-r${bundle.state.recoveryGeneration}`;
|
|
121
|
+
}
|
|
122
|
+
const instructions = await workload.instructions(bundle);
|
|
123
|
+
await mkdir(path.join(bundle.directories.execution, "prompts"), { recursive: true });
|
|
124
|
+
|
|
125
|
+
let validation = { result: null, issues: ["尚未运行 Agent"], protocolVerification: null };
|
|
126
|
+
const firstInvocation = await buildRemoteInvocation({
|
|
127
|
+
bundle,
|
|
128
|
+
attempt: 1,
|
|
129
|
+
validation,
|
|
130
|
+
instructions,
|
|
131
|
+
workloadAdapter: workload,
|
|
132
|
+
});
|
|
133
|
+
const firstResumeInvocation = await buildInfrastructureResumeInvocation({
|
|
134
|
+
bundle,
|
|
135
|
+
instructions,
|
|
136
|
+
targetRecovery: Boolean(config.restoreCheckpointPrefix),
|
|
137
|
+
workloadAdapter: workload,
|
|
138
|
+
});
|
|
139
|
+
const checkpointAttempt = 1;
|
|
140
|
+
const firstPreview = jobPreview({
|
|
141
|
+
bundle,
|
|
142
|
+
invocation: firstInvocation,
|
|
143
|
+
resumeInvocation: firstResumeInvocation,
|
|
144
|
+
attempt: 1,
|
|
145
|
+
config,
|
|
146
|
+
checkpointAttempt,
|
|
147
|
+
restoreCheckpointOnStart: Boolean(config.restoreCheckpointPrefix),
|
|
148
|
+
restoreCheckpointPrefix: config.restoreCheckpointPrefix,
|
|
149
|
+
reuseCompletedResultOnRestore: externalRestoreReuseCompletedResult(config),
|
|
150
|
+
workloadAdapter: workload,
|
|
151
|
+
});
|
|
152
|
+
await recordJobPreview(bundle, firstPreview, 1);
|
|
153
|
+
await writeFile(
|
|
154
|
+
path.join(bundle.directories.execution, "prompts", "attempt-1.txt"),
|
|
155
|
+
`${agentInvocationPromptArgument(firstInvocation)}\n`,
|
|
156
|
+
);
|
|
157
|
+
await writeFile(
|
|
158
|
+
path.join(bundle.directories.execution, "prompts", "attempt-1-resume.txt"),
|
|
159
|
+
`${agentInvocationPromptArgument(firstResumeInvocation)}\n`,
|
|
160
|
+
);
|
|
161
|
+
if (!dryRun) await saveRunRecovery(bundle, gcpOptions.onCheckpoint);
|
|
162
|
+
if (dryRun) {
|
|
163
|
+
await updateRunState(bundle, {
|
|
164
|
+
status: "prepared",
|
|
165
|
+
cloudExecution: publicCloudExecution(config, []),
|
|
166
|
+
});
|
|
167
|
+
return {
|
|
168
|
+
dryRun: true,
|
|
169
|
+
bundle,
|
|
170
|
+
invocation: {
|
|
171
|
+
printable: printableCommand("POST", [`https://batch.googleapis.com/v1/projects/${config.projectId}/locations/${config.region}/jobs?job_id=${firstPreview.jobId}`]),
|
|
172
|
+
executor: "gcp-batch",
|
|
173
|
+
jobId: firstPreview.jobId,
|
|
174
|
+
jobSpec: sanitizeJob(firstPreview.job),
|
|
175
|
+
},
|
|
176
|
+
jobSpec: firstPreview.job,
|
|
177
|
+
cloud: publicCloudExecution(config, []),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const attempts = [];
|
|
182
|
+
const jobs = [];
|
|
183
|
+
const stagedObjects = new Set();
|
|
184
|
+
const checkpointPrefixes = new Set();
|
|
185
|
+
const temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), "citeark-gcp-batch-"));
|
|
186
|
+
let timedOut = false;
|
|
187
|
+
let successful = false;
|
|
188
|
+
let processingLeaseLost = false;
|
|
189
|
+
try {
|
|
190
|
+
await updateRunState(bundle, {
|
|
191
|
+
status: "running",
|
|
192
|
+
startedAt: new Date().toISOString(),
|
|
193
|
+
cloudExecution: publicCloudExecution(config, jobs),
|
|
194
|
+
});
|
|
195
|
+
const controllerValidationRetries = Number.isInteger(workload.controllerValidationRetries)
|
|
196
|
+
? Math.max(0, workload.controllerValidationRetries)
|
|
197
|
+
: bundle.runtimeAgent.validationRetries;
|
|
198
|
+
const maxAttempts = 1 + controllerValidationRetries;
|
|
199
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
200
|
+
const invocation = attempt === 1
|
|
201
|
+
? firstInvocation
|
|
202
|
+
: await buildRemoteInvocation({ bundle, attempt, validation, instructions, workloadAdapter: workload });
|
|
203
|
+
const resumeInvocation = attempt === 1
|
|
204
|
+
? firstResumeInvocation
|
|
205
|
+
: await buildInfrastructureResumeInvocation({ bundle, instructions, workloadAdapter: workload });
|
|
206
|
+
if (attempt > 1) {
|
|
207
|
+
await writeFile(
|
|
208
|
+
path.join(bundle.directories.execution, "prompts", `attempt-${attempt}.txt`),
|
|
209
|
+
`${agentInvocationPromptArgument(invocation)}\n`,
|
|
210
|
+
);
|
|
211
|
+
await writeFile(
|
|
212
|
+
path.join(bundle.directories.execution, "prompts", `attempt-${attempt}-resume.txt`),
|
|
213
|
+
`${agentInvocationPromptArgument(resumeInvocation)}\n`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const basePreview = attempt === 1 ? firstPreview : jobPreview({
|
|
217
|
+
bundle,
|
|
218
|
+
invocation,
|
|
219
|
+
resumeInvocation,
|
|
220
|
+
attempt,
|
|
221
|
+
config,
|
|
222
|
+
checkpointAttempt,
|
|
223
|
+
restoreCheckpointOnStart: true,
|
|
224
|
+
reuseCompletedResultOnRestore: false,
|
|
225
|
+
workloadAdapter: workload,
|
|
226
|
+
});
|
|
227
|
+
const stageObject = basePreview.stageObject;
|
|
228
|
+
const resultObject = basePreview.resultObject;
|
|
229
|
+
stagedObjects.add(stageObject);
|
|
230
|
+
stagedObjects.add(resultObject);
|
|
231
|
+
if (hasSessionEvents()) stagedObjects.add(`${resultObject}.trace.jsonl`);
|
|
232
|
+
stagedObjects.add(basePreview.checkpointStatus);
|
|
233
|
+
checkpointPrefixes.add(basePreview.checkpointPrefix);
|
|
234
|
+
const stageArchive = path.join(temporaryDirectory, basePreview.stageFilename);
|
|
235
|
+
const resultArchive = path.join(temporaryDirectory, basePreview.resultFilename);
|
|
236
|
+
await createStageArchive(bundle, stageArchive);
|
|
237
|
+
progress(`GCP Batch ${bundle.runId}: 上传第 ${attempt}/${maxAttempts} 次执行输入`);
|
|
238
|
+
await clients.storage.uploadFile({
|
|
239
|
+
bucket: config.stagingBucket,
|
|
240
|
+
object: stageObject,
|
|
241
|
+
filePath: stageArchive,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const startedAt = new Date().toISOString();
|
|
245
|
+
try {
|
|
246
|
+
await gcpOptions.assertActive?.();
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (error?.processingLeaseLost === true) processingLeaseLost = true;
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
let preview = basePreview;
|
|
252
|
+
let record = null;
|
|
253
|
+
let terminal = null;
|
|
254
|
+
try {
|
|
255
|
+
({ preview, record, terminal } = await provisionBatchJobAcrossLocations({
|
|
256
|
+
bundle,
|
|
257
|
+
invocation,
|
|
258
|
+
resumeInvocation,
|
|
259
|
+
attempt,
|
|
260
|
+
basePreview,
|
|
261
|
+
config,
|
|
262
|
+
checkpointAttempt,
|
|
263
|
+
restoreCheckpointOnStart: attempt > 1 || Boolean(config.restoreCheckpointPrefix),
|
|
264
|
+
restoreCheckpointPrefix: attempt === 1 ? config.restoreCheckpointPrefix : null,
|
|
265
|
+
reuseCompletedResultOnRestore: attempt === 1
|
|
266
|
+
&& externalRestoreReuseCompletedResult(config),
|
|
267
|
+
workloadAdapter: workload,
|
|
268
|
+
clients,
|
|
269
|
+
jobs,
|
|
270
|
+
gcpOptions,
|
|
271
|
+
progress,
|
|
272
|
+
startedAt,
|
|
273
|
+
}));
|
|
274
|
+
record.state = terminal?.status?.state ?? record.state;
|
|
275
|
+
record.finishedAt = new Date().toISOString();
|
|
276
|
+
record.runDuration = terminal?.status?.runDuration ?? null;
|
|
277
|
+
record.statusEvents = summarizeStatusEvents(terminal?.status?.statusEvents);
|
|
278
|
+
await writeJson(path.join(bundle.directories.execution, `gcp-batch-result-a${attempt}.json`), sanitizeJob(terminal));
|
|
279
|
+
assertSuccessfulGcpBatchTerminal({
|
|
280
|
+
job: terminal,
|
|
281
|
+
jobId: preview.jobId,
|
|
282
|
+
workload: workload.workload,
|
|
283
|
+
infrastructureFailure: summarizeInfrastructureAttempt(terminal).latestFailure,
|
|
284
|
+
});
|
|
285
|
+
await clients.storage.downloadFile({
|
|
286
|
+
bucket: config.stagingBucket,
|
|
287
|
+
object: resultObject,
|
|
288
|
+
filePath: resultArchive,
|
|
289
|
+
});
|
|
290
|
+
await extractResultArchive(bundle, resultArchive);
|
|
291
|
+
const externalObjects = await restoreExternalPublicationObjects({
|
|
292
|
+
bundle,
|
|
293
|
+
storage: clients.storage,
|
|
294
|
+
bucket: config.stagingBucket,
|
|
295
|
+
});
|
|
296
|
+
for (const object of externalObjects) stagedObjects.add(object);
|
|
297
|
+
await updateRunState(bundle, { recoveryRemoteResult: await remoteResultReceipt(bundle, record, attempt) });
|
|
298
|
+
await saveRunRecovery(bundle, gcpOptions.onCheckpoint);
|
|
299
|
+
// The durable result is now on the coordinator. Release the scientific
|
|
300
|
+
// job before parsing, validation or packaging can spend more time.
|
|
301
|
+
if (!config.keepJob) await releaseCompletedBatchJob(clients, record);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
if (error?.processingLeaseLost === true) {
|
|
304
|
+
processingLeaseLost = true;
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
308
|
+
timedOut ||= error?.failureCategory === "timeout"
|
|
309
|
+
|| /超时|执行时限|maximum runtime/i.test(errorMessage);
|
|
310
|
+
record = error?.batchRecord ?? record;
|
|
311
|
+
if (record) {
|
|
312
|
+
record.state = timedOut ? "TIMED_OUT" : record.state;
|
|
313
|
+
record.finishedAt ??= new Date().toISOString();
|
|
314
|
+
record.error ??= errorMessage;
|
|
315
|
+
}
|
|
316
|
+
throw new CiteArkError(`GCP Batch 第 ${attempt} 次执行没有返回可恢复的运行包:${record?.error ?? errorMessage}`, {
|
|
317
|
+
cause: error,
|
|
318
|
+
retryable: error?.retryable,
|
|
319
|
+
failureCode: error?.failureCode,
|
|
320
|
+
failureCategory: error?.failureCategory,
|
|
321
|
+
recoveryAction: error?.recoveryAction,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const remoteAttempt = await readRemoteAttempt(bundle, attempt);
|
|
326
|
+
attempts.push({
|
|
327
|
+
attempt,
|
|
328
|
+
startedAt,
|
|
329
|
+
finishedAt: record.finishedAt,
|
|
330
|
+
exitCode: remoteAttempt?.exitCode ?? (record.state === "SUCCEEDED" ? 0 : 1),
|
|
331
|
+
signal: null,
|
|
332
|
+
timedOut: remoteAttempt?.timedOut === true,
|
|
333
|
+
executor: "gcp-batch",
|
|
334
|
+
batchJob: record.name,
|
|
335
|
+
batchState: record.state,
|
|
336
|
+
infrastructureAttempt: record.infrastructureAttempt,
|
|
337
|
+
infrastructureAttemptLimit: record.infrastructureAttemptLimit,
|
|
338
|
+
provisioningAttempt: record.provisioningAttempt,
|
|
339
|
+
provisioningAttemptLimit: record.provisioningAttemptLimit,
|
|
340
|
+
provisioningLocation: record.provisioningLocation,
|
|
341
|
+
resumeCount: record.resumeCount,
|
|
342
|
+
interruptionCount: record.interruptionCount,
|
|
343
|
+
checkpoint: record.checkpoint,
|
|
344
|
+
});
|
|
345
|
+
const completionPath = path.join(
|
|
346
|
+
bundle.directories.run,
|
|
347
|
+
workload.completionRelativePath,
|
|
348
|
+
);
|
|
349
|
+
if (workload.recoverFinalMessage !== false) {
|
|
350
|
+
await recoverJsonFromFinalMessage({
|
|
351
|
+
finalMessagePath: hostRuntimePath(bundle, CODEX_FINAL_MESSAGE_PATH),
|
|
352
|
+
outputPath: completionPath,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
const submittedValidation = await workload.validate({ bundle, completionPath });
|
|
356
|
+
const remoteFailure = await describeRemoteAgentFailure(bundle, attempt);
|
|
357
|
+
const reconciled = reconcileRemoteAgentOutcome({
|
|
358
|
+
validation: submittedValidation,
|
|
359
|
+
remoteFailure,
|
|
360
|
+
});
|
|
361
|
+
if (reconciled.blockingFailure) {
|
|
362
|
+
throw new CiteArkError(reconciled.blockingFailure.message, {
|
|
363
|
+
retryable: reconciled.blockingFailure.retryable,
|
|
364
|
+
failureCode: reconciled.blockingFailure.failureCode,
|
|
365
|
+
failureCategory: reconciled.blockingFailure.failureCategory,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
validation = reconciled.validation;
|
|
369
|
+
if (validation.issues.length === 0) break;
|
|
370
|
+
if (attempt < maxAttempts) progress(`结果协议校验失败,将在同一远程会话继续:${validation.issues.join(";")}`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
await writeJson(path.join(bundle.directories.execution, "gcp-batch-jobs.json"), {
|
|
374
|
+
schemaVersion: "0.1",
|
|
375
|
+
executor: "gcp-batch",
|
|
376
|
+
...publicCloudExecution(config, jobs),
|
|
377
|
+
});
|
|
378
|
+
await writeGcpInvocation(bundle, jobs, config);
|
|
379
|
+
if (workload.recover) {
|
|
380
|
+
validation = await workload.recover({
|
|
381
|
+
bundle,
|
|
382
|
+
validation,
|
|
383
|
+
attempts,
|
|
384
|
+
timedOut,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
const cloudExecution = publicCloudExecution(config, jobs);
|
|
388
|
+
const result = await workload.finalize({
|
|
389
|
+
bundle,
|
|
390
|
+
validation,
|
|
391
|
+
attempts,
|
|
392
|
+
timedOut,
|
|
393
|
+
sandboxImageIdentity: remoteImageIdentity(bundle.executionEnvironment.image),
|
|
394
|
+
cloudExecution,
|
|
395
|
+
checkpoint: recoveryCheckpointFromJobs(config, jobs),
|
|
396
|
+
});
|
|
397
|
+
successful = workload.isSucceeded(validation.result, validation.issues);
|
|
398
|
+
await saveRunRecovery(bundle, gcpOptions.onCheckpoint);
|
|
399
|
+
return result;
|
|
400
|
+
} catch (error) {
|
|
401
|
+
const recoveryCheckpoint = recoveryCheckpointFromJobs(config, jobs);
|
|
402
|
+
if (recoveryCheckpoint && error && typeof error === "object") {
|
|
403
|
+
error.recoveryCheckpoint = recoveryCheckpoint;
|
|
404
|
+
}
|
|
405
|
+
if (bundle.state.status === "running") {
|
|
406
|
+
await updateRunState(bundle, {
|
|
407
|
+
status: "failed",
|
|
408
|
+
finishedAt: new Date().toISOString(),
|
|
409
|
+
error: error instanceof Error ? error.message : String(error),
|
|
410
|
+
cloudExecution: publicCloudExecution(config, jobs),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
throw error;
|
|
414
|
+
} finally {
|
|
415
|
+
const hasRecoveryCheckpoint = jobs.some((record) =>
|
|
416
|
+
typeof record?.checkpoint?.sequence === "string",
|
|
417
|
+
);
|
|
418
|
+
if (!config.keepJob && (successful || processingLeaseLost || hasRecoveryCheckpoint || jobs.some((record) => record.state === "SUCCEEDED"))) {
|
|
419
|
+
for (const record of jobs) {
|
|
420
|
+
if (record.jobDeletionRequested === true) continue;
|
|
421
|
+
await deleteBatchJob({ batch: clients.batch, record });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
for (const record of jobs) {
|
|
425
|
+
if (record.jobDeletionRequested || new Set(["SUCCEEDED", "FAILED", "CANCELLED"]).has(record.state)) {
|
|
426
|
+
await releaseReproductionAccountLock({ bundle, config, clients, record });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (!config.keepStaging && (successful || processingLeaseLost) && stagedObjects.size > 0) {
|
|
430
|
+
for (const prefix of checkpointPrefixes) {
|
|
431
|
+
const objects = await listCheckpointObjects(
|
|
432
|
+
clients.storage,
|
|
433
|
+
config.stagingBucket,
|
|
434
|
+
prefix,
|
|
435
|
+
);
|
|
436
|
+
for (const object of objects) stagedObjects.add(object);
|
|
437
|
+
}
|
|
438
|
+
const cleanup = await Promise.allSettled([...stagedObjects].map((object) =>
|
|
439
|
+
clients.storage.deleteObject({ bucket: config.stagingBucket, object }),
|
|
440
|
+
));
|
|
441
|
+
const deleted = cleanup.every((result) => result.status === "fulfilled");
|
|
442
|
+
for (const record of jobs) record.stagingObjectsDeleted = deleted;
|
|
443
|
+
}
|
|
444
|
+
await rm(temporaryDirectory, { recursive: true, force: true });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function releaseCompletedBatchJob(clients, record) {
|
|
449
|
+
await deleteBatchJob({ batch: clients.batch, record });
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function assertGcpProvisioningViable(job, config) {
|
|
453
|
+
const capacityEvents = (job?.status?.statusEvents ?? [])
|
|
454
|
+
.map((event) => event?.description)
|
|
455
|
+
.filter((description) =>
|
|
456
|
+
typeof description === "string"
|
|
457
|
+
&& /(?:RESOURCE_POOL_EXHAUSTED|not enough resources|capacity is unavailable|资源不足|容量不足)/i.test(description));
|
|
458
|
+
if (!capacityEvents.length) return;
|
|
459
|
+
const spot = config?.provisioningModel === "SPOT";
|
|
460
|
+
throw new CiteArkError(
|
|
461
|
+
`GCP Batch ${spot ? "Spot" : "Standard"} 容量不足:${capacityEvents.at(-1)}`,
|
|
462
|
+
{
|
|
463
|
+
retryable: true,
|
|
464
|
+
failureCode: spot
|
|
465
|
+
? "gcp.spot_capacity_unavailable"
|
|
466
|
+
: "gcp.standard_capacity_unavailable",
|
|
467
|
+
failureCategory: "infrastructure",
|
|
468
|
+
recoveryAction: spot
|
|
469
|
+
? "保持科研计划不变;先轮换同区域可用区,全部缺货后切换到兼容的 GCP Standard 规格"
|
|
470
|
+
: "保持科研计划与规格不变,轮换同一区域内尚未尝试的可用区",
|
|
471
|
+
},
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function provisionBatchJobAcrossLocations({
|
|
476
|
+
bundle,
|
|
477
|
+
invocation,
|
|
478
|
+
resumeInvocation,
|
|
479
|
+
attempt,
|
|
480
|
+
basePreview,
|
|
481
|
+
config,
|
|
482
|
+
checkpointAttempt,
|
|
483
|
+
restoreCheckpointOnStart,
|
|
484
|
+
restoreCheckpointPrefix,
|
|
485
|
+
reuseCompletedResultOnRestore,
|
|
486
|
+
workloadAdapter,
|
|
487
|
+
clients,
|
|
488
|
+
jobs,
|
|
489
|
+
gcpOptions,
|
|
490
|
+
progress,
|
|
491
|
+
startedAt,
|
|
492
|
+
}) {
|
|
493
|
+
const locations = rotatedLocations(config.allowedLocations, bundle.runId);
|
|
494
|
+
const candidates = locations.length ? locations : [null];
|
|
495
|
+
for (const [index, location] of candidates.entries()) {
|
|
496
|
+
const provisioningAttempt = index + 1;
|
|
497
|
+
const candidateConfig = {
|
|
498
|
+
...config,
|
|
499
|
+
allowedLocations: location ? [location] : [],
|
|
500
|
+
};
|
|
501
|
+
const preview = candidates.length === 1
|
|
502
|
+
? basePreview
|
|
503
|
+
: jobPreview({
|
|
504
|
+
bundle,
|
|
505
|
+
invocation,
|
|
506
|
+
resumeInvocation,
|
|
507
|
+
attempt,
|
|
508
|
+
config: candidateConfig,
|
|
509
|
+
checkpointAttempt,
|
|
510
|
+
restoreCheckpointOnStart,
|
|
511
|
+
restoreCheckpointPrefix,
|
|
512
|
+
reuseCompletedResultOnRestore,
|
|
513
|
+
provisioningAttempt,
|
|
514
|
+
workloadAdapter,
|
|
515
|
+
});
|
|
516
|
+
await recordJobPreview(bundle, preview, attempt, provisioningAttempt);
|
|
517
|
+
const locationLabel = location ?? config.region;
|
|
518
|
+
progress(
|
|
519
|
+
`GCP Batch ${bundle.runId}: 创建 ${preview.jobId}(${config.provisioningModel} / ${config.machineType} / ${locationLabel})`,
|
|
520
|
+
);
|
|
521
|
+
bundle.state.recoveryPendingJob = { jobId: preview.jobId, name: clients.batch.jobName(preview.jobId),
|
|
522
|
+
resultObject: preview.resultObject, checkpointPrefix: preview.checkpointPrefix, checkpointStatus: preview.checkpointStatus,
|
|
523
|
+
attempt, state: "CREATING", startedAt };
|
|
524
|
+
await saveRunRecovery(bundle, gcpOptions.onCheckpoint);
|
|
525
|
+
const created = await createLockedBatchJob({
|
|
526
|
+
storage: clients.storage,
|
|
527
|
+
batch: clients.batch,
|
|
528
|
+
bucket: config.stagingBucket,
|
|
529
|
+
accountId: bundle.runtimeAgent.codexAccountId,
|
|
530
|
+
owner: `${bundle.runId}-${attempt}`,
|
|
531
|
+
jobId: preview.jobId,
|
|
532
|
+
job: preview.job,
|
|
533
|
+
});
|
|
534
|
+
const jobName = created?.name ?? clients.batch.jobName(preview.jobId);
|
|
535
|
+
const record = {
|
|
536
|
+
attempt,
|
|
537
|
+
jobId: preview.jobId,
|
|
538
|
+
name: jobName,
|
|
539
|
+
state: "QUEUED",
|
|
540
|
+
startedAt: provisioningAttempt === 1 ? startedAt : new Date().toISOString(),
|
|
541
|
+
infrastructureAttempt: 1,
|
|
542
|
+
infrastructureAttemptLimit: 1 + config.infrastructureRetryCount,
|
|
543
|
+
provisioningAttempt,
|
|
544
|
+
provisioningAttemptLimit: candidates.length,
|
|
545
|
+
provisioningLocation: location,
|
|
546
|
+
executionAttempt: 1,
|
|
547
|
+
resumeCount: 0,
|
|
548
|
+
interruptionCount: 0,
|
|
549
|
+
latestFailure: null,
|
|
550
|
+
checkpoint: null,
|
|
551
|
+
checkpointPrefix: preview.checkpointPrefix,
|
|
552
|
+
checkpointStatus: preview.checkpointStatus,
|
|
553
|
+
resultObject: preview.resultObject,
|
|
554
|
+
codexAccountLockOwner: `${bundle.runId}-${attempt}`,
|
|
555
|
+
};
|
|
556
|
+
jobs.push(record);
|
|
557
|
+
await updateRunState(bundle, { cloudExecution: publicCloudExecution(config, jobs) });
|
|
558
|
+
await saveRunRecovery(bundle, gcpOptions.onCheckpoint);
|
|
559
|
+
try {
|
|
560
|
+
const terminal = await clients.batch.waitForTerminal(jobName, {
|
|
561
|
+
liveOutput: { storage: clients.storage, bucket: config.stagingBucket, object: `${record.resultObject}.trace.jsonl`, source: `${bundle.runId}:${attempt}`, runtime: bundle.runtimeAgent.runtime },
|
|
562
|
+
timeoutMs: config.waitTimeoutMinutes * 60_000,
|
|
563
|
+
pollIntervalMs: config.pollIntervalMs,
|
|
564
|
+
onState: async (state) => {
|
|
565
|
+
progress(`GCP Batch ${preview.jobId}: ${state}`);
|
|
566
|
+
},
|
|
567
|
+
onPoll: async (job, state) => {
|
|
568
|
+
await gcpOptions.assertActive?.();
|
|
569
|
+
const previousRecovery = `${record.state}:${record.checkpoint?.sequence ?? ""}`;
|
|
570
|
+
await updateCloudJobObservation({
|
|
571
|
+
bundle,
|
|
572
|
+
config: candidateConfig,
|
|
573
|
+
jobs,
|
|
574
|
+
record,
|
|
575
|
+
job,
|
|
576
|
+
state,
|
|
577
|
+
checkpointStatus: preview.checkpointStatus,
|
|
578
|
+
storage: clients.storage,
|
|
579
|
+
progress,
|
|
580
|
+
});
|
|
581
|
+
if (previousRecovery !== `${record.state}:${record.checkpoint?.sequence ?? ""}`) {
|
|
582
|
+
await saveRemoteRecovery({ bundle, storage: clients.storage, bucket: config.stagingBucket, record, persist: gcpOptions.onCheckpoint, recoveryBucket: gcpOptions.recoveryBucket, recoveryNamespace: gcpOptions.recoveryNamespace });
|
|
583
|
+
}
|
|
584
|
+
assertGcpProvisioningViable(job, candidateConfig);
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
// The final recovery checkpoint is written immediately before the
|
|
588
|
+
// supervisor publishes its result. A last status read after Batch has
|
|
589
|
+
// reached a terminal state closes the race where the previous poll saw
|
|
590
|
+
// the result but not the durable target-recovery checkpoint pointer.
|
|
591
|
+
await updateCloudJobObservation({
|
|
592
|
+
bundle,
|
|
593
|
+
config: candidateConfig,
|
|
594
|
+
jobs,
|
|
595
|
+
record,
|
|
596
|
+
job: terminal,
|
|
597
|
+
state: terminal?.status?.state ?? record.state,
|
|
598
|
+
checkpointStatus: preview.checkpointStatus,
|
|
599
|
+
storage: clients.storage,
|
|
600
|
+
progress,
|
|
601
|
+
});
|
|
602
|
+
await saveRemoteRecovery({ bundle, storage: clients.storage, bucket: config.stagingBucket, record, persist: gcpOptions.onCheckpoint, recoveryBucket: gcpOptions.recoveryBucket, recoveryNamespace: gcpOptions.recoveryNamespace });
|
|
603
|
+
await releaseReproductionAccountLock({ bundle, config, clients, record });
|
|
604
|
+
assertGcpProvisioningViable(terminal, candidateConfig);
|
|
605
|
+
return { preview, record, terminal };
|
|
606
|
+
} catch (error) {
|
|
607
|
+
if (error?.processingLeaseLost === true) {
|
|
608
|
+
error.batchRecord = record;
|
|
609
|
+
throw error;
|
|
610
|
+
}
|
|
611
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
612
|
+
record.finishedAt = new Date().toISOString();
|
|
613
|
+
record.error = errorMessage;
|
|
614
|
+
const capacityFailure = /gcp\.(?:spot|standard)_capacity_unavailable/.test(
|
|
615
|
+
error?.failureCode ?? "",
|
|
616
|
+
);
|
|
617
|
+
if (capacityFailure) {
|
|
618
|
+
await deleteCapacityFailedJob({ clients, jobName, record });
|
|
619
|
+
await releaseReproductionAccountLock({ bundle, config, clients, record });
|
|
620
|
+
if (provisioningAttempt < candidates.length) {
|
|
621
|
+
progress(
|
|
622
|
+
`GCP Batch ${preview.jobId}: ${locationLabel} 容量不足,继续尝试下一个可用区`,
|
|
623
|
+
);
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
error.batchRecord = record;
|
|
628
|
+
throw error;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
throw new CiteArkError("GCP Batch 没有可尝试的运行位置", {
|
|
632
|
+
retryable: true,
|
|
633
|
+
failureCode: "gcp.location_unavailable",
|
|
634
|
+
failureCategory: "infrastructure",
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function releaseReproductionAccountLock({ bundle, config, clients, record }) {
|
|
639
|
+
await releaseBatchJobAccountLock({
|
|
640
|
+
storage: clients.storage,
|
|
641
|
+
bucket: config.stagingBucket,
|
|
642
|
+
accountId: bundle.runtimeAgent?.codexAccountId,
|
|
643
|
+
owner: record?.codexAccountLockOwner ?? `${bundle.runId}-${record?.attempt ?? 1}`,
|
|
644
|
+
jobName: record?.name,
|
|
645
|
+
record,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async function deleteCapacityFailedJob({ clients, jobName, record }) {
|
|
650
|
+
if (!record.name) record.name = jobName;
|
|
651
|
+
await deleteBatchJob({ batch: clients.batch, record });
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function rotatedLocations(locations, seed) {
|
|
655
|
+
if (!Array.isArray(locations) || locations.length < 2) return [...(locations ?? [])];
|
|
656
|
+
let hash = 0;
|
|
657
|
+
for (const character of seed) hash = ((hash * 31) + character.charCodeAt(0)) >>> 0;
|
|
658
|
+
const offset = hash % locations.length;
|
|
659
|
+
return [...locations.slice(offset), ...locations.slice(0, offset)];
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function hostRuntimePath(bundle, containerPath) {
|
|
663
|
+
return path.join(bundle.directories.run, containerPath.replace(/^\/job\//, ""));
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async function buildRemoteInvocation({
|
|
667
|
+
bundle,
|
|
668
|
+
attempt,
|
|
669
|
+
validation,
|
|
670
|
+
instructions,
|
|
671
|
+
workloadAdapter,
|
|
672
|
+
}) {
|
|
673
|
+
const invocation = await buildAgentInvocation({
|
|
674
|
+
bundle,
|
|
675
|
+
attempt,
|
|
676
|
+
prompt: workloadAdapter
|
|
677
|
+
? workloadAdapter.prompt({ bundle, attempt, validation })
|
|
678
|
+
: attempt === 1
|
|
679
|
+
? initialReproductionPrompt(bundle)
|
|
680
|
+
: retryReproductionPrompt(validation.issues, bundle.runtimeTask),
|
|
681
|
+
instructions,
|
|
682
|
+
providerSession: remoteProviderSession(bundle.runtimeAgent),
|
|
683
|
+
});
|
|
684
|
+
return workloadAdapter?.wrapInvocation
|
|
685
|
+
? workloadAdapter.wrapInvocation({ bundle, attempt, invocation, validation })
|
|
686
|
+
: invocation;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
async function buildInfrastructureResumeInvocation({
|
|
690
|
+
bundle,
|
|
691
|
+
instructions,
|
|
692
|
+
targetRecovery = false,
|
|
693
|
+
workloadAdapter,
|
|
694
|
+
}) {
|
|
695
|
+
const invocation = await buildAgentInvocation({
|
|
696
|
+
bundle,
|
|
697
|
+
attempt: 2,
|
|
698
|
+
prompt: workloadAdapter
|
|
699
|
+
? workloadAdapter.resumePrompt({ bundle, targetRecovery })
|
|
700
|
+
: targetRecovery
|
|
701
|
+
? targetRecoveryPrompt(bundle)
|
|
702
|
+
: infrastructureResumePrompt(bundle),
|
|
703
|
+
instructions,
|
|
704
|
+
providerSession: remoteProviderSession(bundle.runtimeAgent),
|
|
705
|
+
});
|
|
706
|
+
return workloadAdapter?.wrapInvocation
|
|
707
|
+
? workloadAdapter.wrapInvocation({ bundle, attempt: 2, invocation, targetRecovery })
|
|
708
|
+
: invocation;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function jobPreview({
|
|
712
|
+
bundle,
|
|
713
|
+
invocation,
|
|
714
|
+
resumeInvocation,
|
|
715
|
+
attempt,
|
|
716
|
+
config,
|
|
717
|
+
checkpointAttempt = attempt,
|
|
718
|
+
restoreCheckpointOnStart = false,
|
|
719
|
+
restoreCheckpointPrefix = null,
|
|
720
|
+
reuseCompletedResultOnRestore = true,
|
|
721
|
+
provisioningAttempt = 1,
|
|
722
|
+
workloadAdapter = null,
|
|
723
|
+
}) {
|
|
724
|
+
const normalized = `${bundle.runId}-a${attempt}`
|
|
725
|
+
.toLowerCase()
|
|
726
|
+
.replaceAll(/[^a-z0-9-]/g, "-")
|
|
727
|
+
.replaceAll(/^-+|-+$/g, "");
|
|
728
|
+
const suffix = `${bundle.state.recoveryGeneration ? `-r${bundle.state.recoveryGeneration}` : ""}${provisioningAttempt > 1 ? `-p${provisioningAttempt}` : ""}`;
|
|
729
|
+
const jobId = reproductionBatchJobId(normalized, suffix);
|
|
730
|
+
const stageFilename = `stage-a${attempt}.tar.gz`;
|
|
731
|
+
const resultFilename = `result-a${attempt}.tar.gz`;
|
|
732
|
+
// Keep temporary objects at the bucket root. This avoids relying on
|
|
733
|
+
// implicit directory emulation in GCSFuse, whose CLI changed in v3.
|
|
734
|
+
const objectPrefix = config.stagingPrefix.replaceAll(/[^a-zA-Z0-9_.-]/g, "-");
|
|
735
|
+
const stageObject = `${objectPrefix}-${stageFilename}`;
|
|
736
|
+
const resultObject = `${objectPrefix}-${resultFilename}`;
|
|
737
|
+
const externalObjectPrefix = `${objectPrefix}-result-a${attempt}-object`;
|
|
738
|
+
const checkpoint = resolveCheckpointObjects(config, checkpointAttempt);
|
|
739
|
+
return {
|
|
740
|
+
jobId,
|
|
741
|
+
stageFilename,
|
|
742
|
+
resultFilename,
|
|
743
|
+
stageObject,
|
|
744
|
+
resultObject,
|
|
745
|
+
externalObjectPrefix,
|
|
746
|
+
checkpointPrefix: checkpoint.prefix,
|
|
747
|
+
checkpointStatus: checkpoint.status,
|
|
748
|
+
job: buildGcpBatchJob({
|
|
749
|
+
bundle,
|
|
750
|
+
invocation,
|
|
751
|
+
resumeInvocation,
|
|
752
|
+
attempt,
|
|
753
|
+
config,
|
|
754
|
+
stageObject,
|
|
755
|
+
resultObject,
|
|
756
|
+
externalObjectPrefix,
|
|
757
|
+
checkpointPrefix: checkpoint.prefix,
|
|
758
|
+
checkpointStatus: checkpoint.status,
|
|
759
|
+
restoreCheckpointPrefix,
|
|
760
|
+
restoreCheckpointOnStart,
|
|
761
|
+
reuseCompletedResultOnRestore,
|
|
762
|
+
completionRelativePath: workloadAdapter?.completionRelativePath,
|
|
763
|
+
completionHasEvidence: workloadAdapter?.completionHasEvidence,
|
|
764
|
+
completionValidatorRelativePath: workloadAdapter?.completionValidatorRelativePath,
|
|
765
|
+
completionSuccessJq: workloadAdapter?.completionSuccessJq,
|
|
766
|
+
preserveRebuildableCache: workloadAdapter?.preserveRebuildableCache,
|
|
767
|
+
finalCheckpointKind: workloadAdapter?.finalCheckpointKind,
|
|
768
|
+
finalCheckpointReason: workloadAdapter?.finalCheckpointReason,
|
|
769
|
+
allowCpuHandoff: workloadAdapter?.allowCpuHandoff,
|
|
770
|
+
runtimeBudgetMinutes: workloadAdapter?.runtimeBudgetMinutes,
|
|
771
|
+
workload: workloadAdapter?.workload ?? "reproduction",
|
|
772
|
+
}),
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
export function reproductionBatchJobId(normalized, suffix = "") {
|
|
777
|
+
const limit = 63 - suffix.length;
|
|
778
|
+
const stem = normalized.length <= limit ? normalized
|
|
779
|
+
: `${normalized.slice(0, limit - 13)}-${sha256Value(normalized).slice(0, 12)}`;
|
|
780
|
+
return `${stem}${suffix}`;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
async function recordJobPreview(bundle, preview, attempt, provisioningAttempt = 1) {
|
|
784
|
+
const suffix = provisioningAttempt > 1 ? `-p${provisioningAttempt}` : "";
|
|
785
|
+
await writeJson(path.join(bundle.directories.execution, `gcp-batch-job-a${attempt}${suffix}.json`), {
|
|
786
|
+
schemaVersion: "0.1",
|
|
787
|
+
executor: "gcp-batch",
|
|
788
|
+
jobId: preview.jobId,
|
|
789
|
+
checkpointPrefix: preview.checkpointPrefix,
|
|
790
|
+
checkpointStatus: preview.checkpointStatus,
|
|
791
|
+
job: sanitizeJob(preview.job),
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function recoveryCheckpointFromJobs(config, jobs) {
|
|
796
|
+
const job = [...jobs].reverse().find((candidate) =>
|
|
797
|
+
typeof candidate?.checkpointPrefix === "string"
|
|
798
|
+
&& typeof candidate?.checkpoint?.sequence === "string",
|
|
799
|
+
);
|
|
800
|
+
if (!job) return null;
|
|
801
|
+
return {
|
|
802
|
+
schemaVersion: "0.1",
|
|
803
|
+
provider: "gcp",
|
|
804
|
+
bucket: config.stagingBucket,
|
|
805
|
+
checkpointPrefix: job.checkpointPrefix,
|
|
806
|
+
checkpointStatus: job.checkpointStatus,
|
|
807
|
+
sequence: job.checkpoint.sequence,
|
|
808
|
+
kind: job.checkpoint.kind,
|
|
809
|
+
reason: job.checkpoint.reason,
|
|
810
|
+
object: job.checkpoint.object,
|
|
811
|
+
bytes: job.checkpoint.bytes,
|
|
812
|
+
sha256: job.checkpoint.sha256,
|
|
813
|
+
createdAt: job.checkpoint.createdAt,
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function createClients(config, injected = {}) {
|
|
818
|
+
if (injected.batch && injected.storage) return injected;
|
|
819
|
+
const auth = injected.auth ?? new GoogleAccessTokenProvider(injected.authOptions);
|
|
820
|
+
return {
|
|
821
|
+
batch: injected.batch ?? new GcpBatchClient({
|
|
822
|
+
projectId: config.projectId,
|
|
823
|
+
region: config.region,
|
|
824
|
+
auth,
|
|
825
|
+
...injected.batchOptions,
|
|
826
|
+
}),
|
|
827
|
+
storage: injected.storage ?? new GcsStagingClient({ auth, ...injected.storageOptions }),
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
async function updateCloudJobObservation({
|
|
832
|
+
bundle,
|
|
833
|
+
config,
|
|
834
|
+
jobs,
|
|
835
|
+
record,
|
|
836
|
+
job,
|
|
837
|
+
state,
|
|
838
|
+
checkpointStatus,
|
|
839
|
+
storage,
|
|
840
|
+
progress,
|
|
841
|
+
}) {
|
|
842
|
+
const previousState = record.state;
|
|
843
|
+
const previousInfrastructureAttempt = record.infrastructureAttempt;
|
|
844
|
+
const previousExecutionAttempt = record.executionAttempt;
|
|
845
|
+
const previousCheckpointSequence = record.checkpoint?.sequence ?? null;
|
|
846
|
+
const previousActivityDigest = record.activity?.traceDigest ?? null;
|
|
847
|
+
const infrastructure = summarizeInfrastructureAttempt(job);
|
|
848
|
+
record.state = state;
|
|
849
|
+
record.runDuration = job?.status?.runDuration ?? record.runDuration ?? null;
|
|
850
|
+
record.statusEvents = summarizeStatusEvents(job?.status?.statusEvents);
|
|
851
|
+
record.infrastructureAttempt = infrastructure.infrastructureAttempt;
|
|
852
|
+
record.executionAttempt = infrastructure.executionAttempt;
|
|
853
|
+
record.resumeCount = infrastructure.resumeCount;
|
|
854
|
+
record.interruptionCount = infrastructure.interruptionCount;
|
|
855
|
+
record.latestFailure = infrastructure.latestFailure;
|
|
856
|
+
|
|
857
|
+
if (config.checkpointing.enabled && typeof storage.readJsonObject === "function") {
|
|
858
|
+
const checkpoint = await storage.readJsonObject({
|
|
859
|
+
bucket: config.stagingBucket,
|
|
860
|
+
object: checkpointStatus,
|
|
861
|
+
missing: null,
|
|
862
|
+
}).catch(() => null);
|
|
863
|
+
if (
|
|
864
|
+
checkpoint?.schemaVersion === "0.1"
|
|
865
|
+
&& typeof checkpoint.sequence === "string"
|
|
866
|
+
&& typeof checkpoint.object === "string"
|
|
867
|
+
) {
|
|
868
|
+
const activity = checkpoint.activity;
|
|
869
|
+
const { activity: _privateActivity, ...checkpointRecord } = checkpoint;
|
|
870
|
+
record.checkpoint = checkpointRecord;
|
|
871
|
+
if (
|
|
872
|
+
typeof activity?.traceDigest === "string"
|
|
873
|
+
&& Array.isArray(activity.traceTail)
|
|
874
|
+
) {
|
|
875
|
+
const traceTail = activity.traceTail
|
|
876
|
+
.filter(isPlainObject)
|
|
877
|
+
.map(sanitizeLiveTraceRecord)
|
|
878
|
+
.filter(Boolean)
|
|
879
|
+
.slice(-24);
|
|
880
|
+
const activityChanged = activity.traceDigest !== previousActivityDigest;
|
|
881
|
+
record.activity = {
|
|
882
|
+
traceDigest: activity.traceDigest,
|
|
883
|
+
traceTail,
|
|
884
|
+
lastOutputAt: activityChanged
|
|
885
|
+
? checkpoint.createdAt ?? new Date().toISOString()
|
|
886
|
+
: record.activity?.lastOutputAt ?? null,
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const checkpointChanged = (record.checkpoint?.sequence ?? null) !== previousCheckpointSequence;
|
|
893
|
+
const activityChanged = (record.activity?.traceDigest ?? null) !== previousActivityDigest;
|
|
894
|
+
const infrastructureChanged = record.infrastructureAttempt !== previousInfrastructureAttempt;
|
|
895
|
+
const executionAttemptChanged = record.executionAttempt !== previousExecutionAttempt;
|
|
896
|
+
const stateChanged = state !== previousState;
|
|
897
|
+
if (!checkpointChanged && !activityChanged && !infrastructureChanged && !executionAttemptChanged && !stateChanged) return;
|
|
898
|
+
|
|
899
|
+
if (infrastructureChanged && record.infrastructureAttempt > 1) {
|
|
900
|
+
progress(
|
|
901
|
+
`GCP Batch ${record.jobId}: 检测到可恢复的基础设施故障${record.latestFailure?.exitCode ? `(退出码 ${record.latestFailure.exitCode})` : ""},进入第 ${record.infrastructureAttempt}/${record.infrastructureAttemptLimit} 次执行并从最近检查点恢复`,
|
|
902
|
+
);
|
|
903
|
+
} else if (executionAttemptChanged && record.latestFailure?.category === "timeout") {
|
|
904
|
+
progress(
|
|
905
|
+
`GCP Batch ${record.jobId}: 上一次执行达到不可变时限(退出码 ${record.latestFailure.exitCode});该超时不属于基础设施恢复`,
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
if (checkpointChanged) {
|
|
909
|
+
progress(
|
|
910
|
+
`GCP Batch ${record.jobId}: 已保存${record.checkpoint.kind === "full" ? "完整" : "关键"}检查点 ${record.checkpoint.sequence}`,
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
await updateRunState(bundle, { cloudExecution: publicCloudExecution(config, jobs) });
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function isPlainObject(value) {
|
|
917
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function sanitizeLiveTraceRecord(value) {
|
|
921
|
+
if (!isPlainObject(value) || typeof value.type !== "string") return null;
|
|
922
|
+
const record = compactObject({
|
|
923
|
+
type: value.type,
|
|
924
|
+
timestamp: safeText(value.timestamp, 64),
|
|
925
|
+
recorded_at: safeText(value.recorded_at, 64),
|
|
926
|
+
created_at: safeText(value.created_at, 64),
|
|
927
|
+
attempt: safeNumber(value.attempt),
|
|
928
|
+
executor: safeText(value.executor, 64),
|
|
929
|
+
infrastructureRetry: safeNumber(value.infrastructureRetry),
|
|
930
|
+
restoredCheckpoint: safeNumber(value.restoredCheckpoint),
|
|
931
|
+
kind: safeText(value.kind, 48),
|
|
932
|
+
reason: safeText(value.reason, 120),
|
|
933
|
+
sequence: safeText(value.sequence, 120),
|
|
934
|
+
bytes: safeNumber(value.bytes),
|
|
935
|
+
exitCode: safeNumber(value.exitCode),
|
|
936
|
+
exit_code: safeNumber(value.exit_code),
|
|
937
|
+
timed_out: typeof value.timed_out === "boolean" ? value.timed_out : undefined,
|
|
938
|
+
resultSha256: safeDigest(value.resultSha256),
|
|
939
|
+
signal: safeText(value.signal, 32),
|
|
940
|
+
thread_id: safeText(value.thread_id, 120),
|
|
941
|
+
turn_id: safeText(value.turn_id, 120),
|
|
942
|
+
is_error: typeof value.is_error === "boolean" ? value.is_error : undefined,
|
|
943
|
+
tool_name: safeText(value.tool_name, 80),
|
|
944
|
+
elapsed_time_seconds: safeNumber(value.elapsed_time_seconds),
|
|
945
|
+
});
|
|
946
|
+
if (isPlainObject(value.item)) {
|
|
947
|
+
record.item = compactObject({
|
|
948
|
+
id: safeText(value.item.id, 120),
|
|
949
|
+
type: safeText(value.item.type, 80),
|
|
950
|
+
status: safeText(value.item.status, 48),
|
|
951
|
+
exit_code: safeNumber(value.item.exit_code),
|
|
952
|
+
command: safeText(value.item.command, 300),
|
|
953
|
+
text: safeText(value.item.text, 500),
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
return record;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function compactObject(value) {
|
|
960
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function safeText(value, maximumLength) {
|
|
964
|
+
if (typeof value !== "string") return undefined;
|
|
965
|
+
const redacted = value
|
|
966
|
+
.replaceAll(/sk-[A-Za-z0-9_-]{12,}/g, "<redacted-secret>")
|
|
967
|
+
.replaceAll(/((?:api[_-]?key|token|password|secret)\s*[=:]\s*)[^\s,;]+/gi, "$1<redacted>")
|
|
968
|
+
.replaceAll(/[\r\n\t]+/g, " ")
|
|
969
|
+
.trim();
|
|
970
|
+
return redacted.length > maximumLength
|
|
971
|
+
? `${redacted.slice(0, maximumLength - 1)}…`
|
|
972
|
+
: redacted;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function safeNumber(value) {
|
|
976
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function safeDigest(value) {
|
|
980
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/i.test(value) ? value : undefined;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
async function listCheckpointObjects(storage, bucket, prefix) {
|
|
984
|
+
if (typeof storage.listObjects !== "function") return [];
|
|
985
|
+
return await storage.listObjects({ bucket, prefix: `${prefix}-` }).catch(() => []);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
async function createStageArchive(bundle, destination) {
|
|
989
|
+
await createRemoteStageArchive({
|
|
990
|
+
runDirectory: bundle.directories.run,
|
|
991
|
+
destination,
|
|
992
|
+
label: "GCP",
|
|
993
|
+
paperBudget: bundle.paperBudget,
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
async function extractResultArchive(bundle, source) {
|
|
998
|
+
const result = await captureProcess("tar", ["-xzf", source, "-C", bundle.directories.run]);
|
|
999
|
+
if (result.code !== 0) throw new CiteArkError(`解压 GCP 运行结果失败:\n${result.stderr.trim()}`);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
export async function restoreExternalPublicationObjects({ bundle, storage, bucket }) {
|
|
1003
|
+
const manifestPath = path.join(bundle.directories.execution, "result-export.json");
|
|
1004
|
+
if (!(await pathExists(manifestPath))) return [];
|
|
1005
|
+
const manifest = await readJson(manifestPath);
|
|
1006
|
+
const descriptors = Array.isArray(manifest.externalObjects)
|
|
1007
|
+
? manifest.externalObjects
|
|
1008
|
+
: [];
|
|
1009
|
+
const restored = [];
|
|
1010
|
+
for (const descriptor of descriptors) {
|
|
1011
|
+
const relative = descriptor?.path;
|
|
1012
|
+
const object = descriptor?.object;
|
|
1013
|
+
const digest = descriptor?.sha256;
|
|
1014
|
+
const size = descriptor?.size;
|
|
1015
|
+
if (
|
|
1016
|
+
typeof relative !== "string"
|
|
1017
|
+
|| typeof object !== "string"
|
|
1018
|
+
|| path.posix.basename(object) !== object
|
|
1019
|
+
|| !/^[a-f0-9]{64}$/.test(String(digest))
|
|
1020
|
+
|| !Number.isSafeInteger(size)
|
|
1021
|
+
|| size <= 0
|
|
1022
|
+
) {
|
|
1023
|
+
throw new CiteArkError("远程发布清单包含无效的大对象描述");
|
|
1024
|
+
}
|
|
1025
|
+
const destination = path.resolve(bundle.directories.run, relative);
|
|
1026
|
+
if (!destination.startsWith(`${path.resolve(bundle.directories.run)}${path.sep}`)) {
|
|
1027
|
+
throw new CiteArkError(`远程发布大对象路径逃逸:${relative}`);
|
|
1028
|
+
}
|
|
1029
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
1030
|
+
await storage.downloadFile({ bucket, object, filePath: destination });
|
|
1031
|
+
const [observedDigest, stat] = await Promise.all([
|
|
1032
|
+
sha256File(destination),
|
|
1033
|
+
lstat(destination),
|
|
1034
|
+
]);
|
|
1035
|
+
if (!stat.isFile() || stat.size !== size || observedDigest !== digest) {
|
|
1036
|
+
throw new CiteArkError(`远程发布大对象身份校验失败:${relative}`);
|
|
1037
|
+
}
|
|
1038
|
+
restored.push(object);
|
|
1039
|
+
}
|
|
1040
|
+
return restored;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
async function writeGcpInvocation(bundle, jobs, config) {
|
|
1044
|
+
await writeJson(path.join(bundle.directories.execution, "invocation.json"), {
|
|
1045
|
+
schemaVersion: "0.1",
|
|
1046
|
+
sandbox: "gcp-batch",
|
|
1047
|
+
credentials: {
|
|
1048
|
+
agent: bundle.runtimeAgent.runtime,
|
|
1049
|
+
protocol: bundle.runtimeAgent.api.protocol,
|
|
1050
|
+
source: "gcp-secret-manager",
|
|
1051
|
+
mode: "secret-version-reference",
|
|
1052
|
+
injectedIntoSandbox: true,
|
|
1053
|
+
valueRecorded: false,
|
|
1054
|
+
},
|
|
1055
|
+
networkEgress: { mode: "direct", allowedHosts: [new URL(bundle.runtimeAgent.api.baseUrl).hostname] },
|
|
1056
|
+
cloud: publicCloudExecution(config, jobs),
|
|
1057
|
+
invocations: jobs.map((job) => ({ attempt: job.attempt, batchJob: job.name, state: job.state })),
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function publicCloudExecution(config, jobs) {
|
|
1062
|
+
const runSeconds = jobs.reduce((total, job) => total + durationSeconds(job.runDuration), 0);
|
|
1063
|
+
const billableFloorSeconds = jobs.reduce((total, job) => {
|
|
1064
|
+
const seconds = durationSeconds(job.runDuration);
|
|
1065
|
+
return total + (seconds > 0 ? Math.max(60, seconds) : 0);
|
|
1066
|
+
}, 0);
|
|
1067
|
+
return {
|
|
1068
|
+
provider: "gcp",
|
|
1069
|
+
service: "batch",
|
|
1070
|
+
projectId: config.projectId,
|
|
1071
|
+
region: config.region,
|
|
1072
|
+
machineType: config.machineType,
|
|
1073
|
+
provisioningModel: config.provisioningModel,
|
|
1074
|
+
stagingBucket: config.stagingBucket,
|
|
1075
|
+
stagingPrefix: config.stagingPrefix,
|
|
1076
|
+
checkpointing: {
|
|
1077
|
+
...config.checkpointing,
|
|
1078
|
+
storage: "gcs",
|
|
1079
|
+
durabilityBoundary: "checkpoint-object-closed",
|
|
1080
|
+
},
|
|
1081
|
+
temporaryResources: {
|
|
1082
|
+
cleanupPolicy: {
|
|
1083
|
+
deleteBatchJobs: !config.keepJob,
|
|
1084
|
+
deleteStagingObjects: !config.keepStaging,
|
|
1085
|
+
},
|
|
1086
|
+
batchJobDeletionRequested: config.keepJob ? false : jobs.length ? jobs.every((job) => job.jobDeletionRequested === true) : null,
|
|
1087
|
+
stagingObjectsDeleted: config.keepStaging ? false : jobs.length ? jobs.every((job) => job.stagingObjectsDeleted === true) : null,
|
|
1088
|
+
},
|
|
1089
|
+
credentialInjection: {
|
|
1090
|
+
mode: "runtime-secret-manager-access",
|
|
1091
|
+
valueRecorded: false,
|
|
1092
|
+
batchEnvironmentContainsSecretValue: false,
|
|
1093
|
+
},
|
|
1094
|
+
computeUsage: {
|
|
1095
|
+
jobRunSeconds: runSeconds,
|
|
1096
|
+
billableFloorSeconds,
|
|
1097
|
+
estimatedCostUsd: typeof config.hourlyUsd === "number"
|
|
1098
|
+
? Number(((billableFloorSeconds / 3600) * config.hourlyUsd).toFixed(6))
|
|
1099
|
+
: null,
|
|
1100
|
+
pricingSource: typeof config.hourlyUsd === "number" ? "catalog-rate-x-observed-duration" : "unavailable",
|
|
1101
|
+
invoiceReconciled: false,
|
|
1102
|
+
},
|
|
1103
|
+
jobs: structuredClone(jobs),
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function externalRestoreReuseCompletedResult(config) {
|
|
1108
|
+
return config.restoreCheckpointPrefix
|
|
1109
|
+
? config.reuseCompletedResultOnRestore === true
|
|
1110
|
+
: true;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function sanitizeJob(value) {
|
|
1114
|
+
const clone = structuredClone(value);
|
|
1115
|
+
for (const group of clone?.taskGroups ?? []) {
|
|
1116
|
+
const variables = group?.taskSpec?.environment?.variables;
|
|
1117
|
+
if (variables?.CITEARK_PROVIDER_SECRET_RESOURCE) {
|
|
1118
|
+
variables.CITEARK_PROVIDER_SECRET_RESOURCE = "<secret-manager-version>";
|
|
1119
|
+
}
|
|
1120
|
+
if (variables?.CITEARK_PROVIDER_CREDENTIAL_TOKEN) {
|
|
1121
|
+
variables.CITEARK_PROVIDER_CREDENTIAL_TOKEN = "<short-lived-credential-token>";
|
|
1122
|
+
}
|
|
1123
|
+
const secrets = group?.taskSpec?.environment?.secretVariables;
|
|
1124
|
+
if (secrets) {
|
|
1125
|
+
for (const key of Object.keys(secrets)) secrets[key] = "<secret-manager-version>";
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return clone;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function summarizeStatusEvents(events = []) {
|
|
1132
|
+
return events.slice(-20).map((event) => ({
|
|
1133
|
+
type: event.type ?? null,
|
|
1134
|
+
description: event.description ?? null,
|
|
1135
|
+
eventTime: event.eventTime ?? null,
|
|
1136
|
+
}));
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function remoteImageIdentity(image) {
|
|
1140
|
+
const digest = image.match(/@sha256:[0-9a-f]{64}$/)?.[0]?.slice(1) ?? null;
|
|
1141
|
+
return { id: digest, repositoryDigests: digest ? [image] : [] };
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function durationSeconds(value) {
|
|
1145
|
+
const matched = typeof value === "string" ? value.match(/^(\d+(?:\.\d+)?)s$/) : null;
|
|
1146
|
+
return matched ? Number(matched[1]) : 0;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
async function resumeExistingRemoteResult({ bundle, workload, config, clients, gcpOptions, progress }) {
|
|
1150
|
+
const previous = bundle.state.cloudExecution?.jobs?.at(-1) ?? bundle.state.recoveryPendingJob;
|
|
1151
|
+
if (!previous?.name || typeof clients.batch.findJob !== "function") return null;
|
|
1152
|
+
const cloud = await clients.batch.findJob(previous.name);
|
|
1153
|
+
if (!cloud) return null;
|
|
1154
|
+
progress(`重新连接已有计算任务 ${previous.jobId},不创建重复任务`);
|
|
1155
|
+
const jobs = [previous];
|
|
1156
|
+
const terminal = await clients.batch.waitForTerminal(previous.name, {
|
|
1157
|
+
liveOutput: { storage: clients.storage, bucket: config.stagingBucket, object: `${previous.resultObject}.trace.jsonl`, source: bundle.runId, runtime: bundle.runtimeAgent.runtime },
|
|
1158
|
+
timeoutMs: config.waitTimeoutMinutes * 60_000, pollIntervalMs: config.pollIntervalMs,
|
|
1159
|
+
onPoll: async (job, state) => {
|
|
1160
|
+
await gcpOptions.assertActive?.();
|
|
1161
|
+
const before = `${previous.state}:${previous.checkpoint?.sequence ?? ""}`;
|
|
1162
|
+
await updateCloudJobObservation({ bundle, config, jobs, record: previous, job, state,
|
|
1163
|
+
checkpointStatus: previous.checkpointStatus, storage: clients.storage, progress });
|
|
1164
|
+
if (before !== `${previous.state}:${previous.checkpoint?.sequence ?? ""}`) {
|
|
1165
|
+
await saveRemoteRecovery({ bundle, storage: clients.storage, bucket: config.stagingBucket, record: previous, persist: gcpOptions.onCheckpoint, recoveryBucket: gcpOptions.recoveryBucket, recoveryNamespace: gcpOptions.recoveryNamespace });
|
|
1166
|
+
}
|
|
1167
|
+
},
|
|
1168
|
+
});
|
|
1169
|
+
previous.state = terminal?.status?.state ?? previous.state;
|
|
1170
|
+
await releaseReproductionAccountLock({ bundle, config, clients, record: previous });
|
|
1171
|
+
if (terminal?.status?.state !== "SUCCEEDED") {
|
|
1172
|
+
if (bundle.state.durableRemoteCheckpoint) {
|
|
1173
|
+
await clients.batch.deleteJob(previous.name);
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
assertSuccessfulGcpBatchTerminal({ job: terminal, jobId: previous.jobId, workload: workload.workload });
|
|
1177
|
+
}
|
|
1178
|
+
if (!previous.resultObject) throw new CiteArkError("恢复记录缺少远端结果对象,禁止重复执行");
|
|
1179
|
+
const temporary = await mkdtemp(path.join(os.tmpdir(), "citeark-resume-result-"));
|
|
1180
|
+
try {
|
|
1181
|
+
const archive = path.join(temporary, "result.tar.gz");
|
|
1182
|
+
await clients.storage.downloadFile({ bucket: config.stagingBucket, object: previous.resultObject, filePath: archive });
|
|
1183
|
+
await extractResultArchive(bundle, archive);
|
|
1184
|
+
await restoreExternalPublicationObjects({ bundle, storage: clients.storage, bucket: config.stagingBucket });
|
|
1185
|
+
await updateRunState(bundle, { recoveryRemoteResult: await remoteResultReceipt(bundle, { ...previous, state: "SUCCEEDED", finishedAt: new Date().toISOString() }, previous.attempt) });
|
|
1186
|
+
await saveRunRecovery(bundle, gcpOptions.onCheckpoint);
|
|
1187
|
+
const completionPath = path.join(bundle.directories.run, workload.completionRelativePath);
|
|
1188
|
+
if (workload.recoverFinalMessage !== false) await recoverJsonFromFinalMessage({
|
|
1189
|
+
finalMessagePath: hostRuntimePath(bundle, CODEX_FINAL_MESSAGE_PATH), outputPath: completionPath });
|
|
1190
|
+
const validation = await workload.validate({ bundle, completionPath });
|
|
1191
|
+
const remoteFailure = await describeRemoteAgentFailure(bundle, previous.attempt);
|
|
1192
|
+
const reconciled = reconcileRemoteAgentOutcome({ validation, remoteFailure });
|
|
1193
|
+
if (reconciled.blockingFailure || validation.issues.length) throw new CiteArkError(`已恢复结果,待修复验证:${reconciled.blockingFailure?.message ?? validation.issues.join(";")}`, { failureCode: "recovery.result_validation", retryable: false });
|
|
1194
|
+
const result = await workload.finalize({ bundle, validation: reconciled.validation, attempts: recoveryAttempts(bundle), timedOut: false,
|
|
1195
|
+
sandboxImageIdentity: bundle.state.sandboxImageIdentity ?? remoteImageIdentity(bundle.executionEnvironment.image), cloudExecution: bundle.state.cloudExecution, checkpoint: bundle.state.durableRemoteCheckpoint });
|
|
1196
|
+
await saveRunRecovery(bundle, gcpOptions.onCheckpoint);
|
|
1197
|
+
await clients.batch.deleteJob(previous.name);
|
|
1198
|
+
return result;
|
|
1199
|
+
} finally { await rm(temporary, { recursive: true, force: true }); }
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
async function remoteResultReceipt(bundle, record, attempt) {
|
|
1204
|
+
const remote = await readRemoteAttempt(bundle, attempt);
|
|
1205
|
+
return { attempt, jobName: record.name, downloadedAt: new Date().toISOString(), attemptRecord: {
|
|
1206
|
+
attempt, startedAt: record.startedAt, finishedAt: record.finishedAt,
|
|
1207
|
+
exitCode: remote?.exitCode ?? (record.state === "SUCCEEDED" ? 0 : 1), signal: null,
|
|
1208
|
+
timedOut: remote?.timedOut === true, executor: "gcp-batch", batchJob: record.name, batchState: record.state,
|
|
1209
|
+
infrastructureAttempt: record.infrastructureAttempt, resumeCount: record.resumeCount, checkpoint: record.checkpoint,
|
|
1210
|
+
} };
|
|
1211
|
+
}
|
|
1212
|
+
function recoveryAttempts(bundle) {
|
|
1213
|
+
return bundle.state.attempts?.length ? bundle.state.attempts
|
|
1214
|
+
: bundle.state.recoveryRemoteResult?.attemptRecord ? [bundle.state.recoveryRemoteResult.attemptRecord] : [];
|
|
1215
|
+
}
|