@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,1633 @@
|
|
|
1
|
+
import { freshCodexInvocation } from "../runtime/codex.mjs";
|
|
2
|
+
import { shellQuote } from "../util.mjs";
|
|
3
|
+
|
|
4
|
+
export const WORKSPACE_CHECKPOINT_CAPTURE_PYTHON = String.raw`
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import pathlib
|
|
9
|
+
import shutil
|
|
10
|
+
import stat
|
|
11
|
+
import subprocess
|
|
12
|
+
|
|
13
|
+
root = pathlib.Path(os.environ.get("CITEARK_JOB_ROOT", "/job")).resolve()
|
|
14
|
+
repository = root / "workspace" / "repository"
|
|
15
|
+
execution = root / "execution"
|
|
16
|
+
destination = execution / "workspace-checkpoint"
|
|
17
|
+
temporary = execution / (".workspace-checkpoint-" + str(os.getpid()))
|
|
18
|
+
git = ["git", "-c", "safe.directory=" + str(repository)]
|
|
19
|
+
maximum_file_bytes = 8 * 1024 * 1024
|
|
20
|
+
maximum_total_bytes = 64 * 1024 * 1024
|
|
21
|
+
excluded_directories = {".git", ".cache", ".mypy_cache", ".pytest_cache", ".venv", "__pycache__", "node_modules"}
|
|
22
|
+
|
|
23
|
+
def sha256_file(target):
|
|
24
|
+
digest = hashlib.sha256()
|
|
25
|
+
with target.open("rb") as source:
|
|
26
|
+
while True:
|
|
27
|
+
chunk = source.read(1024 * 1024)
|
|
28
|
+
if not chunk:
|
|
29
|
+
break
|
|
30
|
+
digest.update(chunk)
|
|
31
|
+
return digest.hexdigest()
|
|
32
|
+
|
|
33
|
+
def safe_relative(value):
|
|
34
|
+
candidate = pathlib.PurePosixPath(value)
|
|
35
|
+
return (
|
|
36
|
+
bool(value)
|
|
37
|
+
and not candidate.is_absolute()
|
|
38
|
+
and ".." not in candidate.parts
|
|
39
|
+
and "\\" not in value
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
shutil.rmtree(temporary, ignore_errors=True)
|
|
43
|
+
temporary.mkdir(parents=True)
|
|
44
|
+
(temporary / "untracked").mkdir()
|
|
45
|
+
manifest = {"schemaVersion": "0.1", "retained": [], "excluded": []}
|
|
46
|
+
|
|
47
|
+
inside = subprocess.run(
|
|
48
|
+
[*git, "rev-parse", "--is-inside-work-tree"],
|
|
49
|
+
cwd=repository,
|
|
50
|
+
stdout=subprocess.PIPE,
|
|
51
|
+
stderr=subprocess.DEVNULL,
|
|
52
|
+
text=True,
|
|
53
|
+
)
|
|
54
|
+
git_backed = inside.returncode == 0 and inside.stdout.strip() == "true"
|
|
55
|
+
manifest["mode"] = "git-diff" if git_backed else "runner-snapshot-diff"
|
|
56
|
+
candidate_paths = []
|
|
57
|
+
if git_backed:
|
|
58
|
+
with (temporary / "repository.patch").open("wb") as target:
|
|
59
|
+
diff = subprocess.run(
|
|
60
|
+
[*git, "diff", "--binary", "--no-ext-diff", "HEAD", "--", "."],
|
|
61
|
+
cwd=repository,
|
|
62
|
+
stdout=target,
|
|
63
|
+
stderr=subprocess.PIPE,
|
|
64
|
+
)
|
|
65
|
+
if diff.returncode != 0:
|
|
66
|
+
raise SystemExit("failed to capture workspace patch")
|
|
67
|
+
untracked = subprocess.run(
|
|
68
|
+
[*git, "ls-files", "--others", "--exclude-standard", "-z"],
|
|
69
|
+
cwd=repository,
|
|
70
|
+
stdout=subprocess.PIPE,
|
|
71
|
+
stderr=subprocess.PIPE,
|
|
72
|
+
)
|
|
73
|
+
if untracked.returncode != 0:
|
|
74
|
+
raise SystemExit("failed to list untracked workspace files")
|
|
75
|
+
candidate_paths = [
|
|
76
|
+
raw.decode("utf-8", errors="strict")
|
|
77
|
+
for raw in untracked.stdout.split(b"\0")
|
|
78
|
+
if raw
|
|
79
|
+
]
|
|
80
|
+
else:
|
|
81
|
+
(temporary / "repository.patch").write_bytes(b"")
|
|
82
|
+
before_path = execution / "workspace-before.json"
|
|
83
|
+
if not before_path.is_file():
|
|
84
|
+
raise SystemExit("runner snapshot checkpoint requires workspace-before.json")
|
|
85
|
+
before = json.loads(before_path.read_text(encoding="utf-8"))
|
|
86
|
+
before_files = {
|
|
87
|
+
item.get("path"): item
|
|
88
|
+
for item in before.get("entries", [])
|
|
89
|
+
if isinstance(item, dict) and item.get("type") == "file" and isinstance(item.get("path"), str)
|
|
90
|
+
}
|
|
91
|
+
current_paths = set()
|
|
92
|
+
for source in repository.rglob("*"):
|
|
93
|
+
relative = source.relative_to(repository).as_posix()
|
|
94
|
+
candidate = pathlib.PurePosixPath(relative)
|
|
95
|
+
if any(part in excluded_directories for part in candidate.parts):
|
|
96
|
+
continue
|
|
97
|
+
if source.is_symlink() or not source.is_file():
|
|
98
|
+
continue
|
|
99
|
+
current_paths.add(relative)
|
|
100
|
+
previous = before_files.get(relative)
|
|
101
|
+
size = source.stat().st_size
|
|
102
|
+
if previous and previous.get("size") == size and previous.get("sha256") == sha256_file(source):
|
|
103
|
+
continue
|
|
104
|
+
candidate_paths.append(relative)
|
|
105
|
+
manifest["deleted"] = sorted(set(before_files) - current_paths)
|
|
106
|
+
|
|
107
|
+
if (temporary / "repository.patch").stat().st_size > maximum_total_bytes:
|
|
108
|
+
raise SystemExit("workspace patch exceeds checkpoint boundary")
|
|
109
|
+
|
|
110
|
+
retained_bytes = 0
|
|
111
|
+
for relative in candidate_paths:
|
|
112
|
+
candidate = pathlib.PurePosixPath(relative)
|
|
113
|
+
if not safe_relative(relative) or any(part in excluded_directories for part in candidate.parts):
|
|
114
|
+
manifest["excluded"].append({"path": relative, "reason": "unsafe-or-rebuildable"})
|
|
115
|
+
continue
|
|
116
|
+
if candidate.name == ".env" or candidate.name.startswith(".env."):
|
|
117
|
+
manifest["excluded"].append({"path": relative, "reason": "secret-shaped"})
|
|
118
|
+
continue
|
|
119
|
+
source = repository.joinpath(*candidate.parts)
|
|
120
|
+
if source.is_symlink() or not source.is_file():
|
|
121
|
+
manifest["excluded"].append({"path": relative, "reason": "not-regular-file"})
|
|
122
|
+
continue
|
|
123
|
+
size = source.stat().st_size
|
|
124
|
+
if size > maximum_file_bytes or retained_bytes + size > maximum_total_bytes:
|
|
125
|
+
manifest["excluded"].append({"path": relative, "reason": "rebuildable-size-boundary", "size": size})
|
|
126
|
+
continue
|
|
127
|
+
target = temporary / "untracked" / pathlib.Path(*candidate.parts)
|
|
128
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
129
|
+
shutil.copyfile(source, target)
|
|
130
|
+
mode = stat.S_IMODE(source.stat().st_mode)
|
|
131
|
+
os.chmod(target, mode)
|
|
132
|
+
digest = sha256_file(target)
|
|
133
|
+
manifest["retained"].append({"path": relative, "size": size, "sha256": digest, "mode": mode})
|
|
134
|
+
retained_bytes += size
|
|
135
|
+
|
|
136
|
+
manifest["patchBytes"] = (temporary / "repository.patch").stat().st_size
|
|
137
|
+
manifest["retainedBytes"] = retained_bytes
|
|
138
|
+
(temporary / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
139
|
+
shutil.rmtree(destination, ignore_errors=True)
|
|
140
|
+
temporary.replace(destination)
|
|
141
|
+
`;
|
|
142
|
+
|
|
143
|
+
export const WORKSPACE_CHECKPOINT_RESTORE_PYTHON = String.raw`
|
|
144
|
+
import hashlib
|
|
145
|
+
import json
|
|
146
|
+
import os
|
|
147
|
+
import pathlib
|
|
148
|
+
import shutil
|
|
149
|
+
import subprocess
|
|
150
|
+
|
|
151
|
+
root = pathlib.Path(os.environ.get("CITEARK_JOB_ROOT", "/job")).resolve()
|
|
152
|
+
repository = root / "workspace" / "repository"
|
|
153
|
+
checkpoint = root / "execution" / "workspace-checkpoint"
|
|
154
|
+
manifest_path = checkpoint / "manifest.json"
|
|
155
|
+
patch_path = checkpoint / "repository.patch"
|
|
156
|
+
git = ["git", "-c", "safe.directory=" + str(repository)]
|
|
157
|
+
|
|
158
|
+
def sha256_file(target):
|
|
159
|
+
digest = hashlib.sha256()
|
|
160
|
+
with target.open("rb") as source:
|
|
161
|
+
while True:
|
|
162
|
+
chunk = source.read(1024 * 1024)
|
|
163
|
+
if not chunk:
|
|
164
|
+
break
|
|
165
|
+
digest.update(chunk)
|
|
166
|
+
return digest.hexdigest()
|
|
167
|
+
|
|
168
|
+
def safe_relative(value):
|
|
169
|
+
candidate = pathlib.PurePosixPath(value)
|
|
170
|
+
return (
|
|
171
|
+
bool(value)
|
|
172
|
+
and not candidate.is_absolute()
|
|
173
|
+
and ".." not in candidate.parts
|
|
174
|
+
and "\\" not in value
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if not manifest_path.is_file() or not patch_path.is_file():
|
|
178
|
+
raise SystemExit("workspace checkpoint is incomplete")
|
|
179
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
180
|
+
mode = manifest.get("mode", "git-diff")
|
|
181
|
+
if mode == "git-diff" and patch_path.stat().st_size:
|
|
182
|
+
applied = subprocess.run(
|
|
183
|
+
[*git, "apply", "--binary", "--whitespace=nowarn", str(patch_path)],
|
|
184
|
+
cwd=repository,
|
|
185
|
+
stdout=subprocess.DEVNULL,
|
|
186
|
+
stderr=subprocess.PIPE,
|
|
187
|
+
)
|
|
188
|
+
if applied.returncode != 0:
|
|
189
|
+
raise SystemExit("failed to restore workspace patch")
|
|
190
|
+
elif mode == "runner-snapshot-diff":
|
|
191
|
+
for relative in manifest.get("deleted", []):
|
|
192
|
+
if not isinstance(relative, str) or not safe_relative(relative):
|
|
193
|
+
raise SystemExit("unsafe deleted workspace checkpoint path")
|
|
194
|
+
candidate = pathlib.PurePosixPath(relative)
|
|
195
|
+
target = repository.joinpath(*candidate.parts)
|
|
196
|
+
current = repository
|
|
197
|
+
for part in candidate.parts[:-1]:
|
|
198
|
+
current = current / part
|
|
199
|
+
if current.exists() and current.is_symlink():
|
|
200
|
+
raise SystemExit("workspace checkpoint deletion crosses a symbolic link")
|
|
201
|
+
if target.is_file() and not target.is_symlink():
|
|
202
|
+
target.unlink()
|
|
203
|
+
elif mode != "git-diff":
|
|
204
|
+
raise SystemExit("unknown workspace checkpoint mode")
|
|
205
|
+
|
|
206
|
+
for item in manifest.get("retained", []):
|
|
207
|
+
relative = item.get("path")
|
|
208
|
+
if not isinstance(relative, str) or not safe_relative(relative):
|
|
209
|
+
raise SystemExit("unsafe untracked workspace checkpoint path")
|
|
210
|
+
candidate = pathlib.PurePosixPath(relative)
|
|
211
|
+
source = checkpoint / "untracked" / pathlib.Path(*candidate.parts)
|
|
212
|
+
if source.is_symlink() or not source.is_file():
|
|
213
|
+
raise SystemExit("missing untracked workspace checkpoint file")
|
|
214
|
+
if source.stat().st_size != item.get("size") or sha256_file(source) != item.get("sha256"):
|
|
215
|
+
raise SystemExit("untracked workspace checkpoint identity mismatch")
|
|
216
|
+
target = repository.joinpath(*candidate.parts)
|
|
217
|
+
current = repository
|
|
218
|
+
for part in candidate.parts[:-1]:
|
|
219
|
+
current = current / part
|
|
220
|
+
if current.exists() and current.is_symlink():
|
|
221
|
+
raise SystemExit("workspace checkpoint destination crosses a symbolic link")
|
|
222
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
223
|
+
if target.exists() and target.is_symlink():
|
|
224
|
+
raise SystemExit("workspace checkpoint destination is a symbolic link")
|
|
225
|
+
shutil.copyfile(source, target)
|
|
226
|
+
os.chmod(target, int(item.get("mode", 0o644)) & 0o777)
|
|
227
|
+
`;
|
|
228
|
+
|
|
229
|
+
const WORKSPACE_AUDIT_PYTHON = String.raw`
|
|
230
|
+
import datetime
|
|
231
|
+
import hashlib
|
|
232
|
+
import json
|
|
233
|
+
import os
|
|
234
|
+
import pathlib
|
|
235
|
+
import subprocess
|
|
236
|
+
|
|
237
|
+
root = pathlib.Path(os.environ.get("CITEARK_JOB_ROOT", "/job")).resolve()
|
|
238
|
+
repository = root / "workspace" / "repository"
|
|
239
|
+
execution = root / "execution"
|
|
240
|
+
before_path = execution / "workspace-before.json"
|
|
241
|
+
excluded = {".git", ".cache", ".mypy_cache", ".pytest_cache", ".venv", "__pycache__", "node_modules"}
|
|
242
|
+
|
|
243
|
+
def sha256_file(target):
|
|
244
|
+
digest = hashlib.sha256()
|
|
245
|
+
with target.open("rb") as source:
|
|
246
|
+
while True:
|
|
247
|
+
chunk = source.read(1024 * 1024)
|
|
248
|
+
if not chunk:
|
|
249
|
+
break
|
|
250
|
+
digest.update(chunk)
|
|
251
|
+
return digest.hexdigest()
|
|
252
|
+
|
|
253
|
+
def scan(directory, relative=""):
|
|
254
|
+
entries = []
|
|
255
|
+
for child in sorted(directory.iterdir(), key=lambda item: item.name):
|
|
256
|
+
child_relative = pathlib.PurePosixPath(relative, child.name).as_posix()
|
|
257
|
+
if child.is_symlink():
|
|
258
|
+
entries.append({"path": child_relative, "type": "symlink", "target": os.readlink(child)})
|
|
259
|
+
elif child.is_dir():
|
|
260
|
+
if child.name not in excluded:
|
|
261
|
+
entries.extend(scan(child, child_relative))
|
|
262
|
+
elif child.is_file():
|
|
263
|
+
stat = child.stat()
|
|
264
|
+
entries.append({
|
|
265
|
+
"path": child_relative,
|
|
266
|
+
"type": "file",
|
|
267
|
+
"size": stat.st_size,
|
|
268
|
+
"sha256": sha256_file(child),
|
|
269
|
+
})
|
|
270
|
+
return entries
|
|
271
|
+
|
|
272
|
+
if not repository.is_dir() or not before_path.is_file():
|
|
273
|
+
raise SystemExit("workspace audit requires repository and workspace-before.json")
|
|
274
|
+
|
|
275
|
+
with before_path.open("r", encoding="utf-8") as source:
|
|
276
|
+
before = json.load(source)
|
|
277
|
+
after = {
|
|
278
|
+
"generatedAt": datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
279
|
+
"excludedDirectories": sorted(excluded),
|
|
280
|
+
"entries": scan(repository),
|
|
281
|
+
}
|
|
282
|
+
before_by_path = {entry["path"]: entry for entry in before.get("entries", [])}
|
|
283
|
+
after_by_path = {entry["path"]: entry for entry in after["entries"]}
|
|
284
|
+
changes = {
|
|
285
|
+
"added": [entry for path, entry in after_by_path.items() if path not in before_by_path],
|
|
286
|
+
"modified": [
|
|
287
|
+
{"path": path, "before": before_by_path[path], "after": entry}
|
|
288
|
+
for path, entry in after_by_path.items()
|
|
289
|
+
if path in before_by_path and before_by_path[path] != entry
|
|
290
|
+
],
|
|
291
|
+
"deleted": [entry for path, entry in before_by_path.items() if path not in after_by_path],
|
|
292
|
+
}
|
|
293
|
+
for filename, value in (("workspace-after.json", after), ("workspace-changes.json", changes)):
|
|
294
|
+
temporary = execution / (filename + ".tmp")
|
|
295
|
+
with temporary.open("w", encoding="utf-8") as target:
|
|
296
|
+
json.dump(value, target, ensure_ascii=False, indent=2)
|
|
297
|
+
target.write("\n")
|
|
298
|
+
temporary.replace(execution / filename)
|
|
299
|
+
|
|
300
|
+
patch_path = execution / "repository.patch"
|
|
301
|
+
untracked_path = execution / "untracked-files.txt"
|
|
302
|
+
inside = subprocess.run(
|
|
303
|
+
["git", "rev-parse", "--is-inside-work-tree"],
|
|
304
|
+
cwd=repository,
|
|
305
|
+
stdout=subprocess.PIPE,
|
|
306
|
+
stderr=subprocess.DEVNULL,
|
|
307
|
+
text=True,
|
|
308
|
+
)
|
|
309
|
+
if inside.returncode == 0 and inside.stdout.strip() == "true":
|
|
310
|
+
with patch_path.open("wb") as target:
|
|
311
|
+
diff = subprocess.run(
|
|
312
|
+
["git", "diff", "--binary", "--no-ext-diff", "HEAD"],
|
|
313
|
+
cwd=repository,
|
|
314
|
+
stdout=target,
|
|
315
|
+
stderr=subprocess.PIPE,
|
|
316
|
+
)
|
|
317
|
+
if diff.returncode != 0:
|
|
318
|
+
raise SystemExit("failed to capture repository patch")
|
|
319
|
+
untracked = subprocess.run(
|
|
320
|
+
["git", "ls-files", "--others", "--exclude-standard"],
|
|
321
|
+
cwd=repository,
|
|
322
|
+
stdout=subprocess.PIPE,
|
|
323
|
+
stderr=subprocess.PIPE,
|
|
324
|
+
text=True,
|
|
325
|
+
)
|
|
326
|
+
if untracked.returncode != 0:
|
|
327
|
+
raise SystemExit("failed to capture untracked file list")
|
|
328
|
+
untracked_path.write_text(untracked.stdout, encoding="utf-8")
|
|
329
|
+
else:
|
|
330
|
+
patch_path.write_text("", encoding="utf-8")
|
|
331
|
+
untracked_path.write_text("", encoding="utf-8")
|
|
332
|
+
|
|
333
|
+
if patch_path.stat().st_size > 64 * 1024 * 1024:
|
|
334
|
+
raise SystemExit("repository patch exceeds the 64 MiB publication boundary")
|
|
335
|
+
`;
|
|
336
|
+
|
|
337
|
+
export const PUBLICATION_EXPORT_PYTHON = String.raw`
|
|
338
|
+
import hashlib
|
|
339
|
+
import json
|
|
340
|
+
import os
|
|
341
|
+
import pathlib
|
|
342
|
+
import shutil
|
|
343
|
+
import tarfile
|
|
344
|
+
|
|
345
|
+
root = pathlib.Path(os.environ.get("CITEARK_JOB_ROOT", "/job")).resolve()
|
|
346
|
+
destination = pathlib.Path(os.environ.get("CITEARK_RESULT_ARCHIVE", "/tmp/citeark-result.tar.gz")).resolve()
|
|
347
|
+
completion_relative = os.environ.get("CITEARK_COMPLETION_RELATIVE", "output/result.json")
|
|
348
|
+
maximum_bytes = int(os.environ.get("CITEARK_RESULT_MAX_BYTES", str(512 * 1024 * 1024)))
|
|
349
|
+
inline_evidence_maximum_bytes = int(
|
|
350
|
+
os.environ.get("CITEARK_INLINE_EVIDENCE_MAX_BYTES", str(64 * 1024 * 1024))
|
|
351
|
+
)
|
|
352
|
+
external_object_prefix = os.environ.get("CITEARK_EXTERNAL_OBJECT_PREFIX", "").strip()
|
|
353
|
+
|
|
354
|
+
def safe_relative(value):
|
|
355
|
+
candidate = pathlib.PurePosixPath(str(value))
|
|
356
|
+
return (
|
|
357
|
+
bool(str(value))
|
|
358
|
+
and not candidate.is_absolute()
|
|
359
|
+
and ".." not in candidate.parts
|
|
360
|
+
and "\\" not in str(value)
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def resolve_regular(relative):
|
|
364
|
+
if not safe_relative(relative):
|
|
365
|
+
raise SystemExit("unsafe publication path: " + str(relative))
|
|
366
|
+
source = root / pathlib.PurePosixPath(relative)
|
|
367
|
+
if source.is_symlink():
|
|
368
|
+
raise SystemExit("publication path cannot be a symbolic link: " + str(relative))
|
|
369
|
+
target = source.resolve()
|
|
370
|
+
if root not in target.parents or not target.is_file():
|
|
371
|
+
raise SystemExit("publication path is not a regular file: " + str(relative))
|
|
372
|
+
return target
|
|
373
|
+
|
|
374
|
+
def sha256_file(target):
|
|
375
|
+
digest = hashlib.sha256()
|
|
376
|
+
with target.open("rb") as source:
|
|
377
|
+
while True:
|
|
378
|
+
chunk = source.read(1024 * 1024)
|
|
379
|
+
if not chunk:
|
|
380
|
+
break
|
|
381
|
+
digest.update(chunk)
|
|
382
|
+
return digest.hexdigest()
|
|
383
|
+
|
|
384
|
+
execution = root / "execution"
|
|
385
|
+
manifest_path = execution / "result-export.json"
|
|
386
|
+
previous_manifest = None
|
|
387
|
+
if manifest_path.is_file():
|
|
388
|
+
try:
|
|
389
|
+
previous_manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
390
|
+
except Exception:
|
|
391
|
+
previous_manifest = None
|
|
392
|
+
manifest_path.unlink(missing_ok=True)
|
|
393
|
+
paths = set()
|
|
394
|
+
if execution.is_dir():
|
|
395
|
+
for target in execution.rglob("*"):
|
|
396
|
+
if target.is_file() and not target.is_symlink():
|
|
397
|
+
relative = target.relative_to(root).as_posix()
|
|
398
|
+
if relative != "execution/.publication-reserve":
|
|
399
|
+
paths.add(relative)
|
|
400
|
+
|
|
401
|
+
paths.add(completion_relative)
|
|
402
|
+
completion = resolve_regular(completion_relative)
|
|
403
|
+
declared_relatives = set()
|
|
404
|
+
if completion_relative == "output/result.json":
|
|
405
|
+
try:
|
|
406
|
+
result = json.loads(completion.read_text(encoding="utf-8"))
|
|
407
|
+
except Exception as error:
|
|
408
|
+
raise SystemExit("cannot read output/result.json for evidence export: " + str(error))
|
|
409
|
+
original_result_bytes = completion.read_bytes()
|
|
410
|
+
# Workspace summaries do not duplicate the plan's parser-input manifest.
|
|
411
|
+
# Collect existing inputs here before leaving the scientific machine.
|
|
412
|
+
task_file = root / "input" / "task.json"
|
|
413
|
+
task = json.loads(task_file.read_text(encoding="utf-8")) if task_file.is_file() else {}
|
|
414
|
+
if task.get("workspacePlan", {}).get("version") == 2 and result.get("schemaVersion") == "2.0":
|
|
415
|
+
plan_file = root / "output" / "research-plan.json"
|
|
416
|
+
try:
|
|
417
|
+
plan = json.loads(plan_file.read_text(encoding="utf-8"))
|
|
418
|
+
except (OSError, ValueError):
|
|
419
|
+
plan = {}
|
|
420
|
+
for measurement in plan.get("measurements", []):
|
|
421
|
+
parser_path = measurement.get("parser", {}).get("evidencePath", "")
|
|
422
|
+
relative = "output/" + str(parser_path)
|
|
423
|
+
if not isinstance(parser_path, str) or not safe_relative(parser_path):
|
|
424
|
+
raise SystemExit("unsafe parser publication path: " + relative)
|
|
425
|
+
if (root / relative).is_file():
|
|
426
|
+
resolve_regular(relative)
|
|
427
|
+
declared_relatives.add(relative)
|
|
428
|
+
retained_outputs = []
|
|
429
|
+
retention_excluded = []
|
|
430
|
+
retention_excluded_paths = set()
|
|
431
|
+
for item in result.get("outputs", []):
|
|
432
|
+
if not isinstance(item, dict) or item.get("kind") not in ("model", "checkpoint", "dataset"):
|
|
433
|
+
retained_outputs.append(item)
|
|
434
|
+
continue
|
|
435
|
+
relative = "output/" + str(item.get("path", ""))
|
|
436
|
+
source = resolve_regular(relative)
|
|
437
|
+
retention_excluded_paths.add(item.get("path"))
|
|
438
|
+
retention_excluded.append({
|
|
439
|
+
"id": item.get("id"),
|
|
440
|
+
"kind": item.get("kind"),
|
|
441
|
+
"role": item.get("role"),
|
|
442
|
+
"path": item.get("path"),
|
|
443
|
+
"description": item.get("description"),
|
|
444
|
+
"size": source.stat().st_size,
|
|
445
|
+
"sha256": sha256_file(source),
|
|
446
|
+
"retention": "withheld-model-bytes",
|
|
447
|
+
})
|
|
448
|
+
if retention_excluded:
|
|
449
|
+
result["outputs"] = retained_outputs
|
|
450
|
+
result["evidence"] = [
|
|
451
|
+
item for item in result.get("evidence", [])
|
|
452
|
+
if not isinstance(item, dict) or item.get("path") not in retention_excluded_paths
|
|
453
|
+
]
|
|
454
|
+
retention_record = {
|
|
455
|
+
"schemaVersion": "0.1",
|
|
456
|
+
"policy": "withhold-model-and-checkpoint-bytes",
|
|
457
|
+
"excluded": retention_excluded,
|
|
458
|
+
}
|
|
459
|
+
retention_evidence_relative = "evidence/citeark-publication-retention.json"
|
|
460
|
+
retention_evidence = root / "output" / pathlib.PurePosixPath(retention_evidence_relative)
|
|
461
|
+
retention_evidence.parent.mkdir(parents=True, exist_ok=True)
|
|
462
|
+
retention_evidence.write_text(
|
|
463
|
+
json.dumps(retention_record, ensure_ascii=False, indent=2) + "\n",
|
|
464
|
+
encoding="utf-8",
|
|
465
|
+
)
|
|
466
|
+
result["evidence"].append({
|
|
467
|
+
"path": retention_evidence_relative,
|
|
468
|
+
"description": (
|
|
469
|
+
"CiteArk retention audit for model and checkpoint outputs whose bytes "
|
|
470
|
+
"were intentionally excluded from persistent storage"
|
|
471
|
+
),
|
|
472
|
+
})
|
|
473
|
+
limitation = (
|
|
474
|
+
"Model and checkpoint bytes were intentionally excluded by the CiteArk "
|
|
475
|
+
"retention policy; evaluation evidence and content identities remain in retention audit evidence."
|
|
476
|
+
)
|
|
477
|
+
limitations = result.get("limitations")
|
|
478
|
+
if not isinstance(limitations, list):
|
|
479
|
+
limitations = []
|
|
480
|
+
if limitation not in limitations:
|
|
481
|
+
limitations.append(limitation)
|
|
482
|
+
result["limitations"] = limitations
|
|
483
|
+
completion.write_text(
|
|
484
|
+
json.dumps(result, ensure_ascii=False, indent=2) + "\n",
|
|
485
|
+
encoding="utf-8",
|
|
486
|
+
)
|
|
487
|
+
retention_path = execution / "publication-retention.json"
|
|
488
|
+
retention_path.write_text(
|
|
489
|
+
json.dumps(retention_record, ensure_ascii=False, indent=2) + "\n",
|
|
490
|
+
encoding="utf-8",
|
|
491
|
+
)
|
|
492
|
+
paths.add("execution/publication-retention.json")
|
|
493
|
+
# Output references are also declared evidence. Preserve their real bytes
|
|
494
|
+
# before the coordinator validates the result envelope.
|
|
495
|
+
evidence = result.get("evidence", [])
|
|
496
|
+
declared_evidence_paths = {item.get("path") for item in evidence if isinstance(item, dict)}
|
|
497
|
+
linked_evidence_added = []
|
|
498
|
+
for item in result.get("outputs", []):
|
|
499
|
+
if not isinstance(item, dict):
|
|
500
|
+
continue
|
|
501
|
+
for related in item.get("relatedEvidencePaths", []):
|
|
502
|
+
if not isinstance(related, str) or related in declared_evidence_paths:
|
|
503
|
+
continue
|
|
504
|
+
if related in retention_excluded_paths:
|
|
505
|
+
continue
|
|
506
|
+
resolve_regular("output/" + related)
|
|
507
|
+
evidence.append({"path": related, "description": "Existing evidence referenced by output " + str(item.get("id", "unknown"))})
|
|
508
|
+
declared_evidence_paths.add(related)
|
|
509
|
+
linked_evidence_added.append(related)
|
|
510
|
+
if linked_evidence_added:
|
|
511
|
+
original_path = execution / "result-envelope-original.json"
|
|
512
|
+
if not original_path.exists():
|
|
513
|
+
original_path.write_bytes(original_result_bytes)
|
|
514
|
+
paths.add("execution/result-envelope-original.json")
|
|
515
|
+
result["evidence"] = evidence
|
|
516
|
+
completion.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
517
|
+
audit_path = execution / "result-envelope-normalization.json"
|
|
518
|
+
audit_path.write_text(json.dumps({"kind": "citeark.result-envelope-normalization", "addedReferencedEvidence": linked_evidence_added}) + "\n", encoding="utf-8")
|
|
519
|
+
paths.add("execution/result-envelope-normalization.json")
|
|
520
|
+
for collection in ("evidence", "outputs"):
|
|
521
|
+
for item in result.get(collection, []):
|
|
522
|
+
if not isinstance(item, dict) or not isinstance(item.get("path"), str):
|
|
523
|
+
continue
|
|
524
|
+
relative = "output/" + item["path"]
|
|
525
|
+
if not safe_relative(relative):
|
|
526
|
+
raise SystemExit("unsafe declared publication path: " + relative)
|
|
527
|
+
declared_relatives.add(relative)
|
|
528
|
+
|
|
529
|
+
# Raw scientific tables and histories are evidence even when the Agent only
|
|
530
|
+
# declares their aggregate. Do not lose them when the scientific VM is removed.
|
|
531
|
+
raw_suffixes = {".json", ".jsonl", ".ndjson", ".csv", ".tsv", ".txt", ".log", ".parquet", ".arrow"}
|
|
532
|
+
output_root = root / "output"
|
|
533
|
+
withheld_paths = {item.get("path") for item in result.get("outputs", [])
|
|
534
|
+
if isinstance(item, dict) and item.get("kind") in ("model", "checkpoint", "dataset")} if completion_relative == "output/result.json" else set()
|
|
535
|
+
if completion_relative == "output/result.json":
|
|
536
|
+
withheld_paths.update(retention_excluded_paths)
|
|
537
|
+
for target in output_root.rglob("*"):
|
|
538
|
+
if not target.is_file() or target.is_symlink() or target.suffix.lower() not in raw_suffixes:
|
|
539
|
+
continue
|
|
540
|
+
relative_output = target.relative_to(output_root).as_posix()
|
|
541
|
+
if relative_output in withheld_paths or any(part.startswith(".env") or part.lower() in ("auth.json", "credentials.json", "secrets.json") for part in target.relative_to(output_root).parts):
|
|
542
|
+
continue
|
|
543
|
+
relative = target.relative_to(root).as_posix()
|
|
544
|
+
resolve_regular(relative)
|
|
545
|
+
declared_relatives.add(relative)
|
|
546
|
+
|
|
547
|
+
for optional in ("output/environment.txt", "output/packaging-recovery.txt", "output/experiment-records.jsonl", "output/cpu-preparation.json",
|
|
548
|
+
"output/research-plan.json", "output/progress.md", "output/rejected-research-plan.json"):
|
|
549
|
+
target = root / optional
|
|
550
|
+
if target.is_file() and not target.is_symlink():
|
|
551
|
+
paths.add(optional)
|
|
552
|
+
|
|
553
|
+
# The working plan drives downstream parsing, even if the Agent forgot to list
|
|
554
|
+
# it as evidence. Export it and its revisions independently of result.evidence.
|
|
555
|
+
task_path = root / "input" / "task.json"
|
|
556
|
+
if task_path.is_file():
|
|
557
|
+
task = json.loads(task_path.read_text(encoding="utf-8"))
|
|
558
|
+
if isinstance(task.get("workspacePlan"), dict) and task["workspacePlan"].get("version") in (1, 2):
|
|
559
|
+
resolve_regular("output/research-plan.json")
|
|
560
|
+
paths.add("output/research-plan.json")
|
|
561
|
+
plan_history = root / "output" / "plan-history"
|
|
562
|
+
if plan_history.is_dir() and not plan_history.is_symlink():
|
|
563
|
+
for revision in plan_history.glob("*.json"):
|
|
564
|
+
if revision.is_file() and not revision.is_symlink():
|
|
565
|
+
paths.add(revision.relative_to(root).as_posix())
|
|
566
|
+
phase_history = root / "output" / "phase-history"
|
|
567
|
+
if phase_history.is_dir() and not phase_history.is_symlink():
|
|
568
|
+
for receipt in phase_history.glob("*.json"):
|
|
569
|
+
resolve_regular(receipt.relative_to(root).as_posix())
|
|
570
|
+
paths.add(receipt.relative_to(root).as_posix())
|
|
571
|
+
experiment_logs = root / "output" / "experiment-logs"
|
|
572
|
+
if experiment_logs.is_dir() and not experiment_logs.is_symlink():
|
|
573
|
+
for log in experiment_logs.iterdir():
|
|
574
|
+
if log.name.endswith((".stdout.log", ".stderr.log")) and log.is_file() and not log.is_symlink():
|
|
575
|
+
relative = log.relative_to(root).as_posix()
|
|
576
|
+
resolve_regular(relative)
|
|
577
|
+
paths.add(relative)
|
|
578
|
+
|
|
579
|
+
previous_external = {}
|
|
580
|
+
if isinstance(previous_manifest, dict):
|
|
581
|
+
for descriptor in previous_manifest.get("externalObjects", []):
|
|
582
|
+
if not isinstance(descriptor, dict):
|
|
583
|
+
continue
|
|
584
|
+
relative = descriptor.get("path")
|
|
585
|
+
object_name = descriptor.get("object")
|
|
586
|
+
digest = descriptor.get("sha256")
|
|
587
|
+
size = descriptor.get("size")
|
|
588
|
+
if (
|
|
589
|
+
isinstance(relative, str)
|
|
590
|
+
and safe_relative(relative)
|
|
591
|
+
and isinstance(object_name, str)
|
|
592
|
+
and pathlib.PurePosixPath(object_name).name == object_name
|
|
593
|
+
and isinstance(digest, str)
|
|
594
|
+
and len(digest) == 64
|
|
595
|
+
and all(character in "0123456789abcdef" for character in digest)
|
|
596
|
+
and isinstance(size, int)
|
|
597
|
+
and size > 0
|
|
598
|
+
):
|
|
599
|
+
previous_external[relative] = descriptor
|
|
600
|
+
|
|
601
|
+
external = []
|
|
602
|
+
inline_used = sum(resolve_regular(relative).stat().st_size for relative in paths)
|
|
603
|
+
for relative in sorted(declared_relatives):
|
|
604
|
+
source = root / pathlib.PurePosixPath(relative)
|
|
605
|
+
if source.is_file() and not source.is_symlink():
|
|
606
|
+
size = source.stat().st_size
|
|
607
|
+
if relative in paths:
|
|
608
|
+
continue
|
|
609
|
+
if size <= inline_evidence_maximum_bytes and inline_used + size <= maximum_bytes:
|
|
610
|
+
paths.add(relative)
|
|
611
|
+
inline_used += size
|
|
612
|
+
continue
|
|
613
|
+
if not external_object_prefix:
|
|
614
|
+
raise SystemExit("large declared output requires CITEARK_EXTERNAL_OBJECT_PREFIX: " + relative)
|
|
615
|
+
digest = sha256_file(source)
|
|
616
|
+
external_target = pathlib.Path(external_object_prefix + "-" + digest + ".blob")
|
|
617
|
+
external_target.parent.mkdir(parents=True, exist_ok=True)
|
|
618
|
+
shutil.copyfile(source, external_target)
|
|
619
|
+
external.append({
|
|
620
|
+
"path": relative,
|
|
621
|
+
"size": size,
|
|
622
|
+
"sha256": digest,
|
|
623
|
+
"transport": "gcs-external",
|
|
624
|
+
"object": external_target.name,
|
|
625
|
+
})
|
|
626
|
+
continue
|
|
627
|
+
reusable = previous_external.get(relative)
|
|
628
|
+
if reusable is None:
|
|
629
|
+
raise SystemExit("publication path is not a regular file: " + relative)
|
|
630
|
+
external.append(reusable)
|
|
631
|
+
|
|
632
|
+
descriptors = []
|
|
633
|
+
total = 0
|
|
634
|
+
for relative in sorted(paths):
|
|
635
|
+
target = resolve_regular(relative)
|
|
636
|
+
size = target.stat().st_size
|
|
637
|
+
total += size
|
|
638
|
+
if total > maximum_bytes:
|
|
639
|
+
raise SystemExit(
|
|
640
|
+
"publication result exceeds the configured byte boundary; large non-model outputs must use content-addressed object storage"
|
|
641
|
+
)
|
|
642
|
+
descriptors.append({
|
|
643
|
+
"path": relative,
|
|
644
|
+
"size": size,
|
|
645
|
+
"sha256": sha256_file(target),
|
|
646
|
+
"transport": "inline",
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
manifest_path.write_text(
|
|
650
|
+
json.dumps({
|
|
651
|
+
"schemaVersion": "0.1",
|
|
652
|
+
"kind": "citeark.publication-result-export",
|
|
653
|
+
"contentPolicy": "scientific-tables-histories-declared-outputs-and-runner-audit",
|
|
654
|
+
"workspaceIncluded": False,
|
|
655
|
+
"runtimeHomeIncluded": False,
|
|
656
|
+
"byteSize": total,
|
|
657
|
+
"externalByteSize": sum(item["size"] for item in external),
|
|
658
|
+
"files": descriptors,
|
|
659
|
+
"externalObjects": external,
|
|
660
|
+
}, ensure_ascii=False, indent=2) + "\n",
|
|
661
|
+
encoding="utf-8",
|
|
662
|
+
)
|
|
663
|
+
paths.add("execution/result-export.json")
|
|
664
|
+
|
|
665
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
666
|
+
with tarfile.open(destination, "w:gz", dereference=False) as archive:
|
|
667
|
+
for relative in sorted(paths):
|
|
668
|
+
archive.add(resolve_regular(relative), arcname=relative, recursive=False)
|
|
669
|
+
`;
|
|
670
|
+
|
|
671
|
+
// Keep the primary attempt diagnosis when exporting its diagnostic bundle also
|
|
672
|
+
// fails. A missing research.json after a stalled Agent is a secondary failure.
|
|
673
|
+
export const PUBLICATION_FAILURE_JQ = `
|
|
674
|
+
. + {exitCode:$exitCode, publicationExitCode:$exitCode,
|
|
675
|
+
publicationFailureStage:$publicationFailureStage, finishedAt:$finishedAt}
|
|
676
|
+
+ (if (.agentExitCode // 0) != 0 then {}
|
|
677
|
+
elif $storageExhausted == 1 then
|
|
678
|
+
{retryable:true, failureCategory:"infrastructure",
|
|
679
|
+
failureStage:$publicationFailureStage, failureCode:"infrastructure.storage_exhausted"}
|
|
680
|
+
else {retryable:false, failureCategory:"publication", failureStage:$publicationFailureStage}
|
|
681
|
+
end)
|
|
682
|
+
`;
|
|
683
|
+
|
|
684
|
+
export const PUBLICATION_FAILURE_EXPORT_PYTHON = String.raw`
|
|
685
|
+
import io
|
|
686
|
+
import json
|
|
687
|
+
import os
|
|
688
|
+
import pathlib
|
|
689
|
+
import tarfile
|
|
690
|
+
|
|
691
|
+
root = pathlib.Path(os.environ.get("CITEARK_JOB_ROOT", "/job")).resolve()
|
|
692
|
+
destination = pathlib.Path(os.environ.get("CITEARK_RESULT_ARCHIVE", "/tmp/citeark-result.tar.gz")).resolve()
|
|
693
|
+
candidates = [
|
|
694
|
+
"output/result.json",
|
|
695
|
+
"output/research-plan.json",
|
|
696
|
+
"output/progress.md",
|
|
697
|
+
"execution/trace.jsonl",
|
|
698
|
+
"execution/runner-events.jsonl",
|
|
699
|
+
"execution/claude-stderr.log",
|
|
700
|
+
"execution/provider-pro-relay.log",
|
|
701
|
+
"execution/network-router.log",
|
|
702
|
+
"execution/liguse-tunnel.log",
|
|
703
|
+
"execution/network-preflight.json",
|
|
704
|
+
"execution/runner-boundary.json",
|
|
705
|
+
"execution/runtime-budget.json",
|
|
706
|
+
"execution/agent-stalled.json",
|
|
707
|
+
"execution/paper-budget-exhausted.json",
|
|
708
|
+
"execution/publication-stderr.log",
|
|
709
|
+
"execution/checkpoint.log",
|
|
710
|
+
"execution/disk-usage.jsonl",
|
|
711
|
+
"execution/result-ready.json",
|
|
712
|
+
]
|
|
713
|
+
execution = root / "execution"
|
|
714
|
+
if execution.is_dir():
|
|
715
|
+
candidates.extend(
|
|
716
|
+
target.relative_to(root).as_posix()
|
|
717
|
+
for target in execution.glob("remote-attempt-*.json")
|
|
718
|
+
if target.is_file() and not target.is_symlink()
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
maximum_file_bytes = int(os.environ.get("CITEARK_FAILURE_FILE_MAX_BYTES", str(8 * 1024 * 1024)))
|
|
722
|
+
maximum_total_bytes = int(os.environ.get("CITEARK_FAILURE_TOTAL_MAX_BYTES", str(64 * 1024 * 1024)))
|
|
723
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
724
|
+
destination.unlink(missing_ok=True)
|
|
725
|
+
retained = []
|
|
726
|
+
retained_bytes = 0
|
|
727
|
+
with tarfile.open(destination, "w:gz", dereference=False) as archive:
|
|
728
|
+
for relative in candidates:
|
|
729
|
+
target = (root / relative).resolve()
|
|
730
|
+
if root in target.parents and target.is_file() and not target.is_symlink():
|
|
731
|
+
remaining = maximum_total_bytes - retained_bytes
|
|
732
|
+
if remaining <= 0:
|
|
733
|
+
break
|
|
734
|
+
size = target.stat().st_size
|
|
735
|
+
keep = min(size, maximum_file_bytes, remaining)
|
|
736
|
+
with target.open("rb") as source:
|
|
737
|
+
if size > keep:
|
|
738
|
+
source.seek(size - keep)
|
|
739
|
+
payload = source.read(keep)
|
|
740
|
+
info = tarfile.TarInfo(relative + (".tail" if size > keep else ""))
|
|
741
|
+
info.size = len(payload)
|
|
742
|
+
info.mode = 0o600
|
|
743
|
+
archive.addfile(info, io.BytesIO(payload))
|
|
744
|
+
retained_bytes += len(payload)
|
|
745
|
+
retained.append({"path": relative, "sourceBytes": size, "retainedBytes": len(payload), "truncated": size > keep})
|
|
746
|
+
manifest = json.dumps({
|
|
747
|
+
"schemaVersion": "0.1",
|
|
748
|
+
"kind": "citeark.bounded-publication-failure-export",
|
|
749
|
+
"maximumFileBytes": maximum_file_bytes,
|
|
750
|
+
"maximumTotalBytes": maximum_total_bytes,
|
|
751
|
+
"retainedBytes": retained_bytes,
|
|
752
|
+
"files": retained,
|
|
753
|
+
}, ensure_ascii=False, indent=2).encode("utf-8") + b"\n"
|
|
754
|
+
info = tarfile.TarInfo("execution/failure-export.json")
|
|
755
|
+
info.size = len(manifest)
|
|
756
|
+
info.mode = 0o600
|
|
757
|
+
archive.addfile(info, io.BytesIO(manifest))
|
|
758
|
+
`;
|
|
759
|
+
|
|
760
|
+
export function buildCheckpointedWorkerScript({
|
|
761
|
+
runId = "unknown-run",
|
|
762
|
+
executor = "gcp-batch",
|
|
763
|
+
initialInvocation,
|
|
764
|
+
resumeInvocation,
|
|
765
|
+
attempt,
|
|
766
|
+
stagePath,
|
|
767
|
+
resultPath,
|
|
768
|
+
liveTracePath,
|
|
769
|
+
streamTrace = false,
|
|
770
|
+
checkpointPrefixPath,
|
|
771
|
+
checkpointStatusPath,
|
|
772
|
+
externalObjectPrefixPath = `${resultPath}.object`,
|
|
773
|
+
restoreCheckpointPrefixPath = checkpointPrefixPath,
|
|
774
|
+
checkpointPolicy,
|
|
775
|
+
runtimeBudgetSeconds = 0,
|
|
776
|
+
completionRelativePath = "output/result.json",
|
|
777
|
+
completionHasEvidence = true,
|
|
778
|
+
completionValidatorRelativePath = null,
|
|
779
|
+
completionSuccessJq = '.execution.status == "succeeded"',
|
|
780
|
+
preserveRebuildableCache = false,
|
|
781
|
+
finalCheckpointKind = "critical",
|
|
782
|
+
finalCheckpointReason = "publication-ready",
|
|
783
|
+
allowCpuHandoff = false,
|
|
784
|
+
providerRelay = false,
|
|
785
|
+
providerRelayPath = "/opt/citeark/codex-pro-relay.mjs",
|
|
786
|
+
agentStallTimeoutSeconds = 0,
|
|
787
|
+
restoreCheckpointOnStart = false,
|
|
788
|
+
reuseCompletedResultOnRestore = true,
|
|
789
|
+
}) {
|
|
790
|
+
const initialPromptPath = `/tmp/citeark-active-prompts/attempt-${attempt}.txt`;
|
|
791
|
+
const resumePromptPath = `/tmp/citeark-active-prompts/attempt-${attempt}-resume.txt`;
|
|
792
|
+
const initialCommand = commandLine(initialInvocation, initialPromptPath);
|
|
793
|
+
const resumeCommand = commandLine(resumeInvocation, resumePromptPath);
|
|
794
|
+
const freshCodexCommand = commandLine(freshCodexInvocation(initialInvocation), initialPromptPath);
|
|
795
|
+
const enabled = checkpointPolicy.enabled ? "1" : "0";
|
|
796
|
+
const packageCacheRoot = executor === "autodl-pro"
|
|
797
|
+
? "/opt/citeark/cache"
|
|
798
|
+
: "/job/runtime-cache";
|
|
799
|
+
const agentEnvironment = [
|
|
800
|
+
...(providerRelay ? ["OPENAI_API_KEY=citeark-local-relay", "CITEARK_API_KEY=citeark-local-relay"] : []),
|
|
801
|
+
"HOME=/job/runtime-home",
|
|
802
|
+
"PATH=/job/runtime-venv/bin:/opt/citeark/venv/bin:/opt/citeark/cargo/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
803
|
+
"VIRTUAL_ENV=/job/runtime-venv",
|
|
804
|
+
"UV_PROJECT_ENVIRONMENT=/job/runtime-venv",
|
|
805
|
+
"UV_CONFIG_FILE=/dev/null",
|
|
806
|
+
'PIP_INDEX_URL="$PYTHON_PACKAGE_INDEX"',
|
|
807
|
+
'UV_INDEX="$PYTHON_PACKAGE_INDEX"',
|
|
808
|
+
"UV_INDEX_STRATEGY=first-index",
|
|
809
|
+
"PIP_DEFAULT_TIMEOUT=120",
|
|
810
|
+
"PIP_RETRIES=8",
|
|
811
|
+
"UV_HTTP_TIMEOUT=120",
|
|
812
|
+
"UV_HTTP_RETRIES=8",
|
|
813
|
+
'HF_ENDPOINT="$HUGGINGFACE_ENDPOINT"',
|
|
814
|
+
"HF_HUB_ETAG_TIMEOUT=30",
|
|
815
|
+
"HF_HUB_DOWNLOAD_TIMEOUT=120",
|
|
816
|
+
"UV_PYTHON_INSTALL_DIR=/job/runtime-cache/uv-python",
|
|
817
|
+
`UV_CACHE_DIR=${packageCacheRoot}/uv`,
|
|
818
|
+
`PIP_CACHE_DIR=${packageCacheRoot}/pip`,
|
|
819
|
+
`CARGO_HOME=${packageCacheRoot}/cargo`,
|
|
820
|
+
"CARGO_NET_GIT_FETCH_WITH_CLI=true",
|
|
821
|
+
"XDG_CACHE_HOME=/job/runtime-cache/xdg",
|
|
822
|
+
"HF_HOME=/job/runtime-cache/huggingface",
|
|
823
|
+
"TORCH_HOME=/job/runtime-cache/torch",
|
|
824
|
+
].join(" ");
|
|
825
|
+
return [
|
|
826
|
+
"set -Eeuo pipefail",
|
|
827
|
+
"JOB_ROOT=/job",
|
|
828
|
+
`RUN_ID=${shellQuote(runId)}`,
|
|
829
|
+
`STAGE=${shellQuote(stagePath)}`,
|
|
830
|
+
`RESULT=${shellQuote(resultPath)}`,
|
|
831
|
+
`CHECKPOINT_PREFIX=${shellQuote(checkpointPrefixPath)}`,
|
|
832
|
+
`CHECKPOINT_STATUS=${shellQuote(checkpointStatusPath)}`,
|
|
833
|
+
`EXTERNAL_OBJECT_PREFIX=${shellQuote(externalObjectPrefixPath)}`,
|
|
834
|
+
`RESTORE_CHECKPOINT_PREFIX=${shellQuote(restoreCheckpointPrefixPath)}`,
|
|
835
|
+
`CHECKPOINT_ENABLED=${enabled}`,
|
|
836
|
+
`CHECKPOINT_CRITICAL_INTERVAL=${checkpointPolicy.criticalIntervalSeconds}`,
|
|
837
|
+
`CHECKPOINT_FULL_INTERVAL=${checkpointPolicy.fullIntervalSeconds}`,
|
|
838
|
+
`CHECKPOINT_RETENTION=${checkpointPolicy.retention}`,
|
|
839
|
+
`TOTAL_RUNTIME_BUDGET_SECONDS=${Math.max(0, Math.floor(Number(runtimeBudgetSeconds) || 0))}`,
|
|
840
|
+
`RESTORE_CHECKPOINT_ON_START=${restoreCheckpointOnStart ? "1" : "0"}`,
|
|
841
|
+
`REUSE_COMPLETED_RESULT_ON_RESTORE=${reuseCompletedResultOnRestore ? "1" : "0"}`,
|
|
842
|
+
"RESULT_READY=/job/execution/result-ready.json",
|
|
843
|
+
"INVALIDATED_RESULT_SHA256=",
|
|
844
|
+
`COMPLETION=${shellQuote(`/job/${completionRelativePath}`)}`,
|
|
845
|
+
`COMPLETION_RELATIVE=${shellQuote(completionRelativePath)}`,
|
|
846
|
+
`COMPLETION_HAS_EVIDENCE=${completionHasEvidence ? "1" : "0"}`,
|
|
847
|
+
`COMPLETION_VALIDATOR=${shellQuote(completionValidatorRelativePath ? `/job/${completionValidatorRelativePath}` : "")}`,
|
|
848
|
+
`COMPLETION_SUCCESS_JQ=${shellQuote(completionSuccessJq)}`,
|
|
849
|
+
`PRESERVE_REBUILDABLE_CACHE=${preserveRebuildableCache ? "1" : "0"}`,
|
|
850
|
+
`FINAL_CHECKPOINT_KIND=${shellQuote(finalCheckpointKind)}`,
|
|
851
|
+
`FINAL_CHECKPOINT_REASON=${shellQuote(finalCheckpointReason)}`,
|
|
852
|
+
`ALLOW_CPU_HANDOFF=${allowCpuHandoff ? "1" : "0"}`,
|
|
853
|
+
`PROVIDER_RELAY=${providerRelay ? "1" : "0"}`,
|
|
854
|
+
`PROVIDER_RELAY_PATH=${shellQuote(providerRelayPath)}`,
|
|
855
|
+
`AGENT_STALL_TIMEOUT_SECONDS=${Math.max(0, Math.floor(Number(agentStallTimeoutSeconds) || 0))}`,
|
|
856
|
+
"INFRASTRUCTURE_RETRY=${BATCH_TASK_RETRY_ATTEMPT:-0}",
|
|
857
|
+
"CHECKPOINT_STOP=/tmp/citeark-checkpoint-stop",
|
|
858
|
+
"CHECKPOINT_LOCK=/tmp/citeark-checkpoint-lock",
|
|
859
|
+
"AGENT_PID_FILE=/tmp/citeark-agent.pid",
|
|
860
|
+
"AGENT_STALL_STATE=/job/execution/agent-stalled.json",
|
|
861
|
+
"AGENT_PID=",
|
|
862
|
+
"AGENT_STALLED=0",
|
|
863
|
+
"AGENT_TIMED_OUT=0",
|
|
864
|
+
"AGENT_RUN_USER=",
|
|
865
|
+
"RUNTIME_VIRTUAL_ENV=/job/runtime-venv",
|
|
866
|
+
"SUPERVISOR_PYTHON=/opt/citeark/venv/bin/python",
|
|
867
|
+
"PYTHON_PACKAGE_INDEX=",
|
|
868
|
+
"HUGGINGFACE_ENDPOINT=",
|
|
869
|
+
"RUNTIME_BUDGET_STATE=/job/execution/runtime-budget.json",
|
|
870
|
+
"PUBLICATION_RESERVE=/job/execution/.publication-reserve",
|
|
871
|
+
"RUNTIME_BUDGET_BASE_SECONDS=0",
|
|
872
|
+
"RUNTIME_BUDGET_SEGMENT_STARTED_EPOCH=0",
|
|
873
|
+
"PROVIDER_RELAY_PID=",
|
|
874
|
+
shellFunctions(executor),
|
|
875
|
+
"mkdir -p \"$JOB_ROOT\"",
|
|
876
|
+
"cp \"$STAGE\" /tmp/citeark-stage.tar.gz",
|
|
877
|
+
"tar -xzf /tmp/citeark-stage.tar.gz -C \"$JOB_ROOT\"",
|
|
878
|
+
"mkdir -p /job/output /job/execution /job/runtime-home/codex /job/assets /job/runtime-cache/cargo /job/runtime-cache/uv /job/runtime-cache/uv-python /job/runtime-cache/pip /job/runtime-cache/xdg /job/runtime-cache/huggingface /job/runtime-cache/torch",
|
|
879
|
+
"fallocate -l 128M \"$PUBLICATION_RESERVE\" 2>/dev/null || dd if=/dev/zero of=\"$PUBLICATION_RESERVE\" bs=1M count=128 status=none",
|
|
880
|
+
// Checkpoints contain the previous phase's execution/prompts. Keep this
|
|
881
|
+
// coordinator's invocation outside /job before restoring that history.
|
|
882
|
+
"# BEGIN current invocation prompts",
|
|
883
|
+
"mkdir -p /tmp/citeark-active-prompts",
|
|
884
|
+
`cp /job/execution/prompts/attempt-${attempt}.txt ${initialPromptPath}`,
|
|
885
|
+
`cp /job/execution/prompts/attempt-${attempt}-resume.txt ${resumePromptPath}`,
|
|
886
|
+
"# END current invocation prompts",
|
|
887
|
+
"if [ -f /job/input/runner-codex-capacity-retry.mjs ]; then cp /job/input/runner-codex-capacity-retry.mjs /tmp/citeark-codex-capacity-retry.mjs; fi",
|
|
888
|
+
"initialize_scientific_runtime",
|
|
889
|
+
"cp /job/input/runner-codex-account.mjs /tmp/citeark-codex-account.mjs",
|
|
890
|
+
"trap 'node /tmp/citeark-codex-account.mjs close' EXIT",
|
|
891
|
+
"hydrate_provider_secret",
|
|
892
|
+
"if [ -f /job/input/runner-paper-budget.json ]; then chown root:root /job/input/runner-paper-budget.json; chmod 0600 /job/input/runner-paper-budget.json; fi",
|
|
893
|
+
"if [ \"$PROVIDER_RELAY\" = 1 ]; then start_provider_relay; fi",
|
|
894
|
+
"RESTORED_CHECKPOINT=0",
|
|
895
|
+
"if [ \"$CHECKPOINT_ENABLED\" = 1 ] && { [ \"$INFRASTRUCTURE_RETRY\" -gt 0 ] || [ \"$RESTORE_CHECKPOINT_ON_START\" = 1 ]; }; then restore_checkpoints; fi",
|
|
896
|
+
`if [ -s "$AGENT_STALL_STATE" ]; then mv "$AGENT_STALL_STATE" "/job/execution/agent-stalled-before-a${attempt}-r$INFRASTRUCTURE_RETRY.json"; fi`,
|
|
897
|
+
"initialize_runtime_budget",
|
|
898
|
+
"if [ \"$RESTORED_CHECKPOINT\" = 1 ] && [ \"$REUSE_COMPLETED_RESULT_ON_RESTORE\" != 1 ]; then",
|
|
899
|
+
" if [ -s \"$COMPLETION\" ]; then INVALIDATED_RESULT_SHA256=\"$(sha256sum \"$COMPLETION\" | cut -d' ' -f1)\"; fi",
|
|
900
|
+
" if [ -n \"$INVALIDATED_RESULT_SHA256\" ]; then mkdir -p /job/output/phase-history; cp \"$COMPLETION\" \"/job/output/phase-history/$INVALIDATED_RESULT_SHA256.json\"; fi",
|
|
901
|
+
" rm -f \"$RESULT_READY\"",
|
|
902
|
+
" printf '{\"type\":\"citeark.completed_result_invalidated\",\"reason\":\"semantic-validation-retry\",\"timestamp\":\"%s\"}\\n' \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" >> /job/execution/trace.jsonl",
|
|
903
|
+
"fi",
|
|
904
|
+
"configure_runner_boundary",
|
|
905
|
+
"BUDGET_WATCH_PID=",
|
|
906
|
+
"rm -f /job/execution/paper-budget-exhausted.json",
|
|
907
|
+
"if [ \"${CITEARK_PAPER_BUDGET_REQUIRED:-0}\" = 1 ]; then",
|
|
908
|
+
|
|
909
|
+
" ( while true; do if [ -s /job/execution/paper-budget-exhausted.json ]; then pkill -TERM -u node || true; sleep 15; pkill -KILL -u node || true; break; fi; sleep 2; done ) &",
|
|
910
|
+
" BUDGET_WATCH_PID=$!",
|
|
911
|
+
"fi",
|
|
912
|
+
"if ! ensure_disk_headroom 20 pre-agent; then",
|
|
913
|
+
` jq -nc --argjson attempt ${attempt} --argjson infrastructureRetry "$INFRASTRUCTURE_RETRY" --arg finishedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '{attempt:$attempt,exitCode:75,agentExitCode:0,infrastructureRetry:$infrastructureRetry,restoredCheckpoint:false,timedOut:false,retryable:true,failureCategory:"infrastructure",failureStage:"setup",failureCode:"infrastructure.storage_headroom_insufficient",finishedAt:$finishedAt}' > /job/execution/remote-attempt-${attempt}.json`,
|
|
914
|
+
" release_rebuildable_runtime_cache",
|
|
915
|
+
" rm -f \"$PUBLICATION_RESERVE\" /tmp/citeark-result.tar.gz",
|
|
916
|
+
` CITEARK_JOB_ROOT=/job CITEARK_RESULT_ARCHIVE=/tmp/citeark-result.tar.gz "$SUPERVISOR_PYTHON" -c ${shellQuote(PUBLICATION_FAILURE_EXPORT_PYTHON)}`,
|
|
917
|
+
" cp /tmp/citeark-result.tar.gz \"$RESULT\"",
|
|
918
|
+
" sync",
|
|
919
|
+
" exit 0",
|
|
920
|
+
"fi",
|
|
921
|
+
`printf '{"type":"citeark.attempt_started","attempt":${attempt},"executor":${JSON.stringify(executor)},"infrastructureRetry":%s,"restoredCheckpoint":%s,"timestamp":"%s"}\\n' "$INFRASTRUCTURE_RETRY" "$RESTORED_CHECKPOINT" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> /job/execution/trace.jsonl`,
|
|
922
|
+
"rm -f \"$CHECKPOINT_STOP\" \"$AGENT_PID_FILE\" \"$AGENT_STALL_STATE\"",
|
|
923
|
+
"checkpoint_loop & CHECKPOINT_PID=$!",
|
|
924
|
+
"trap 'handle_termination' TERM INT",
|
|
925
|
+
"set +e",
|
|
926
|
+
"if [ \"$RESTORED_CHECKPOINT\" = 1 ] && [ \"$REUSE_COMPLETED_RESULT_ON_RESTORE\" = 1 ] && result_ready_matches; then",
|
|
927
|
+
" printf '{\"type\":\"citeark.completed_result_reused\",\"infrastructureRetry\":%s,\"timestamp\":\"%s\"}\\n' \"$INFRASTRUCTURE_RETRY\" \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" >> /job/execution/trace.jsonl",
|
|
928
|
+
" AGENT_EXIT=0",
|
|
929
|
+
"else",
|
|
930
|
+
" rm -f \"$RESULT_READY\"",
|
|
931
|
+
" if [ \"$RESTORED_CHECKPOINT\" = 1 ]; then AGENT_COMMAND=" + shellQuote(resumeCommand) + "; else AGENT_COMMAND=" + shellQuote(initialCommand) + "; fi",
|
|
932
|
+
" if [ -n \"${CITEARK_CODEX_ACCOUNT_ID:-}\" ] && ! find /job/runtime-home/codex/sessions -name '*.jsonl' -print -quit 2>/dev/null | grep -q .; then AGENT_COMMAND=" + shellQuote(freshCodexCommand) + "; fi",
|
|
933
|
+
" cd /job/workspace/repository",
|
|
934
|
+
" REMAINING_RUNTIME_SECONDS=\"$(remaining_runtime_budget_seconds)\"",
|
|
935
|
+
" if [ \"$TOTAL_RUNTIME_BUDGET_SECONDS\" -gt 0 ] && [ \"$REMAINING_RUNTIME_SECONDS\" -le 0 ]; then",
|
|
936
|
+
" AGENT_EXIT=124",
|
|
937
|
+
" AGENT_TIMED_OUT=1",
|
|
938
|
+
" printf '{\"type\":\"citeark.runtime_budget_exhausted\",\"runtimeBudgetSeconds\":%s,\"timestamp\":\"%s\"}\\n' \"$TOTAL_RUNTIME_BUDGET_SECONDS\" \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" >> /job/execution/trace.jsonl",
|
|
939
|
+
" else",
|
|
940
|
+
` RUNNER_EVENT_ID="$RUN_ID:${attempt}:$INFRASTRUCTURE_RETRY"`,
|
|
941
|
+
" RUNNER_STARTED_AT=\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"",
|
|
942
|
+
` jq -nc --arg eventId "$RUNNER_EVENT_ID" --arg runId "$RUN_ID" --argjson attempt ${attempt} --arg executor ${shellQuote(executor)} --arg command "$AGENT_COMMAND" --arg startedAt "$RUNNER_STARTED_AT" --arg captureBoundary "$RUNNER_CAPTURE_BOUNDARY" '{schemaVersion:"0.1",type:"runner.process_started",$eventId,$runId,$attempt,$executor,command:"/bin/bash",args:["-lc",$command],workingDirectory:"/job/workspace/repository",$captureBoundary,$startedAt}' >> /job/execution/runner-events.jsonl`,
|
|
943
|
+
" if [ -n \"$AGENT_RUN_USER\" ]; then",
|
|
944
|
+
` if [ "$TOTAL_RUNTIME_BUDGET_SECONDS" -gt 0 ]; then timeout --signal=TERM --kill-after=15s "\${REMAINING_RUNTIME_SECONDS}s" runuser -u "$AGENT_RUN_USER" -- env ${agentEnvironment} /bin/bash -lc "exec $AGENT_COMMAND" >> /job/execution/trace.jsonl 2>> /job/execution/claude-stderr.log & else runuser -u "$AGENT_RUN_USER" -- env ${agentEnvironment} /bin/bash -lc "exec $AGENT_COMMAND" >> /job/execution/trace.jsonl 2>> /job/execution/claude-stderr.log & fi`,
|
|
945
|
+
" else",
|
|
946
|
+
" if [ \"$TOTAL_RUNTIME_BUDGET_SECONDS\" -gt 0 ]; then timeout --signal=TERM --kill-after=15s \"${REMAINING_RUNTIME_SECONDS}s\" /bin/bash -lc \"exec $AGENT_COMMAND\" >> /job/execution/trace.jsonl 2>> /job/execution/claude-stderr.log & else /bin/bash -lc \"exec $AGENT_COMMAND\" >> /job/execution/trace.jsonl 2>> /job/execution/claude-stderr.log & fi",
|
|
947
|
+
" fi",
|
|
948
|
+
" AGENT_PID=$!",
|
|
949
|
+
" printf '%s\\n' \"$AGENT_PID\" > \"$AGENT_PID_FILE\"",
|
|
950
|
+
...(streamTrace ? [' tail -c +1 --pid="$AGENT_PID" -f /job/execution/trace.jsonl &', ' LIVE_TRACE_PID=$!'] : []),
|
|
951
|
+
...(liveTracePath ? [
|
|
952
|
+
` (while kill -0 "$AGENT_PID" 2>/dev/null; do cp /job/execution/trace.jsonl ${shellQuote(liveTracePath)} 2>/dev/null || true; sleep 2; done) &`,
|
|
953
|
+
' LIVE_TRACE_PID=$!',
|
|
954
|
+
] : []),
|
|
955
|
+
" wait \"$AGENT_PID\"",
|
|
956
|
+
" AGENT_EXIT=$?",
|
|
957
|
+
...(streamTrace ? [' wait "$LIVE_TRACE_PID" || true'] : []),
|
|
958
|
+
...(liveTracePath ? [' kill "$LIVE_TRACE_PID" 2>/dev/null || true', ` cp /job/execution/trace.jsonl ${shellQuote(liveTracePath)} 2>/dev/null || true`] : []),
|
|
959
|
+
" if [ -s \"$AGENT_STALL_STATE\" ]; then AGENT_STALLED=1; AGENT_TIMED_OUT=1; AGENT_EXIT=124; fi",
|
|
960
|
+
" if [ \"$AGENT_EXIT\" -eq 124 ]; then AGENT_TIMED_OUT=1; fi",
|
|
961
|
+
" RUNNER_FINISHED_AT=\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"",
|
|
962
|
+
` jq -nc --arg eventId "$RUNNER_EVENT_ID" --arg runId "$RUN_ID" --argjson attempt ${attempt} --arg executor ${shellQuote(executor)} --argjson exitCode "$AGENT_EXIT" --argjson timedOut "$AGENT_TIMED_OUT" --arg startedAt "$RUNNER_STARTED_AT" --arg finishedAt "$RUNNER_FINISHED_AT" '{schemaVersion:"0.1",type:"runner.process_finished",$eventId,$runId,$attempt,$executor,$exitCode,signal:null,$timedOut,$startedAt,$finishedAt}' >> /job/execution/runner-events.jsonl`,
|
|
963
|
+
" rm -f \"$AGENT_PID_FILE\"",
|
|
964
|
+
" AGENT_PID=",
|
|
965
|
+
" fi",
|
|
966
|
+
"fi",
|
|
967
|
+
"set -e",
|
|
968
|
+
"touch \"$CHECKPOINT_STOP\"",
|
|
969
|
+
"if [ -n \"${CHECKPOINT_PID:-}\" ]; then wait \"$CHECKPOINT_PID\" || true; fi",
|
|
970
|
+
"stop_provider_relay",
|
|
971
|
+
"if [ -n \"${BUDGET_WATCH_PID:-}\" ]; then kill \"$BUDGET_WATCH_PID\" 2>/dev/null || true; fi",
|
|
972
|
+
"write_runtime_budget || true",
|
|
973
|
+
"if [ \"$AGENT_EXIT\" -eq 0 ] && [ -n \"$COMPLETION_VALIDATOR\" ]; then",
|
|
974
|
+
" if ! \"$SUPERVISOR_PYTHON\" \"$COMPLETION_VALIDATOR\" /job/input/asset-preparation-plan.json \"$COMPLETION\" >> /job/execution/completion-validation.log 2>&1; then AGENT_EXIT=65; fi",
|
|
975
|
+
"fi",
|
|
976
|
+
"if [ \"$AGENT_EXIT\" -eq 0 ]; then mark_result_ready agent-completed || true; fi",
|
|
977
|
+
"RECOVERY_NEEDED=0",
|
|
978
|
+
"if [ \"$ALLOW_CPU_HANDOFF\" = 1 ] && jq -e '.schemaVersion == \"2.0\" and .status == \"handoff\" and .nextPhase == \"cpu\"' \"$COMPLETION\" >/dev/null 2>&1; then FINAL_CHECKPOINT_KIND=full; FINAL_CHECKPOINT_REASON=research-handoff; PRESERVE_REBUILDABLE_CACHE=1; fi",
|
|
979
|
+
"if [ \"$AGENT_EXIT\" -ne 0 ] || [ ! -s \"$COMPLETION\" ]; then RECOVERY_NEEDED=1; elif ! jq -e \"$COMPLETION_SUCCESS_JQ\" \"$COMPLETION\" >/dev/null 2>&1; then RECOVERY_NEEDED=1; fi",
|
|
980
|
+
"if [ \"$CHECKPOINT_ENABLED\" = 1 ] && [ \"$RECOVERY_NEEDED\" = 1 ]; then checkpoint_state full target-recovery || true; fi",
|
|
981
|
+
"if [ \"$CHECKPOINT_ENABLED\" = 1 ] && [ \"$RECOVERY_NEEDED\" = 0 ] && [ \"$PRESERVE_REBUILDABLE_CACHE\" = 1 ]; then checkpoint_state \"$FINAL_CHECKPOINT_KIND\" \"$FINAL_CHECKPOINT_REASON\" || true; fi",
|
|
982
|
+
`jq -nc --argjson attempt ${attempt} --argjson exitCode "$AGENT_EXIT" --argjson infrastructureRetry "$INFRASTRUCTURE_RETRY" --argjson restoredCheckpoint "$RESTORED_CHECKPOINT" --argjson timedOut "$AGENT_TIMED_OUT" --argjson agentStalled "$AGENT_STALLED" --argjson runtimeBudgetSeconds "$TOTAL_RUNTIME_BUDGET_SECONDS" --argjson consumedRuntimeSeconds "$(runtime_budget_consumed_seconds)" --arg finishedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '{attempt:$attempt,exitCode:$exitCode,agentExitCode:$exitCode,infrastructureRetry:$infrastructureRetry,restoredCheckpoint:$restoredCheckpoint,timedOut:$timedOut,agentStalled:$agentStalled,runtimeBudgetSeconds:$runtimeBudgetSeconds,consumedRuntimeSeconds:$consumedRuntimeSeconds,finishedAt:$finishedAt} + (if $agentStalled == 1 then {failureCategory:"timeout",failureStage:"provider",failureCode:"agent.effective_progress_stalled",retryable:false} elif $timedOut == 1 then {failureCategory:"timeout",failureStage:"execution",failureCode:"execution.runtime_budget_exhausted",retryable:false} else {} end)' > /job/execution/remote-attempt-${attempt}.json`,
|
|
983
|
+
`printf '{"type":"citeark.attempt_finished","attempt":${attempt},"executor":${JSON.stringify(executor)},"exitCode":%s,"infrastructureRetry":%s,"timestamp":"%s"}\\n' "$AGENT_EXIT" "$INFRASTRUCTURE_RETRY" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> /job/execution/trace.jsonl`,
|
|
984
|
+
"if [ \"$PRESERVE_REBUILDABLE_CACHE\" != 1 ]; then release_rebuildable_runtime_cache; fi",
|
|
985
|
+
"PUBLICATION_STAGE=workspace-audit",
|
|
986
|
+
"PUBLICATION_EXIT=0",
|
|
987
|
+
": > /job/execution/publication-stderr.log",
|
|
988
|
+
"if ! ensure_disk_headroom 1 pre-publication; then PUBLICATION_STAGE=storage-headroom; PUBLICATION_EXIT=75; printf '%s\\n' 'No space left on device: publication headroom below 1 GiB' >> /job/execution/publication-stderr.log; fi",
|
|
989
|
+
`if [ "$PUBLICATION_EXIT" -eq 0 ]; then CITEARK_JOB_ROOT=/job "$SUPERVISOR_PYTHON" -c ${shellQuote(WORKSPACE_AUDIT_PYTHON)} 2>> /job/execution/publication-stderr.log || PUBLICATION_EXIT=$?; fi`,
|
|
990
|
+
"if [ \"$PUBLICATION_EXIT\" -eq 0 ]; then",
|
|
991
|
+
" PUBLICATION_STAGE=result-export",
|
|
992
|
+
` CITEARK_JOB_ROOT=/job CITEARK_RESULT_ARCHIVE=/tmp/citeark-result.tar.gz CITEARK_COMPLETION_RELATIVE="$COMPLETION_RELATIVE" CITEARK_EXTERNAL_OBJECT_PREFIX="$EXTERNAL_OBJECT_PREFIX" "$SUPERVISOR_PYTHON" -c ${shellQuote(PUBLICATION_EXPORT_PYTHON)} 2>> /job/execution/publication-stderr.log || PUBLICATION_EXIT=$?`,
|
|
993
|
+
"fi",
|
|
994
|
+
"if [ \"$PUBLICATION_EXIT\" -ne 0 ]; then",
|
|
995
|
+
" STORAGE_EXHAUSTED=0",
|
|
996
|
+
" if grep -Eqi 'No space left on device|ENOSPC|storage headroom' /job/execution/publication-stderr.log; then STORAGE_EXHAUSTED=1; fi",
|
|
997
|
+
` jq --argjson exitCode "$PUBLICATION_EXIT" --argjson storageExhausted "$STORAGE_EXHAUSTED" --arg publicationFailureStage "$PUBLICATION_STAGE" --arg finishedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" ${shellQuote(PUBLICATION_FAILURE_JQ)} /job/execution/remote-attempt-${attempt}.json > /job/execution/remote-attempt-${attempt}.json.tmp`,
|
|
998
|
+
` mv /job/execution/remote-attempt-${attempt}.json.tmp /job/execution/remote-attempt-${attempt}.json`,
|
|
999
|
+
" rm -f /tmp/citeark-result.tar.gz \"$PUBLICATION_RESERVE\"",
|
|
1000
|
+
" if [ \"$PRESERVE_REBUILDABLE_CACHE\" != 1 ]; then release_rebuildable_runtime_cache; fi",
|
|
1001
|
+
` CITEARK_JOB_ROOT=/job CITEARK_RESULT_ARCHIVE=/tmp/citeark-result.tar.gz "$SUPERVISOR_PYTHON" -c ${shellQuote(PUBLICATION_FAILURE_EXPORT_PYTHON)}`,
|
|
1002
|
+
" cp /tmp/citeark-result.tar.gz \"$RESULT\"",
|
|
1003
|
+
" sync",
|
|
1004
|
+
" exit 0",
|
|
1005
|
+
"fi",
|
|
1006
|
+
"rm -f \"$PUBLICATION_RESERVE\"",
|
|
1007
|
+
"if [ \"$CHECKPOINT_ENABLED\" = 1 ] && [ \"$PRESERVE_REBUILDABLE_CACHE\" != 1 ]; then checkpoint_state \"$FINAL_CHECKPOINT_KIND\" \"$FINAL_CHECKPOINT_REASON\" || true; fi",
|
|
1008
|
+
"cp /tmp/citeark-result.tar.gz \"$RESULT\"",
|
|
1009
|
+
"sync",
|
|
1010
|
+
"exit 0",
|
|
1011
|
+
].join("\n");
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function commandLine(invocation, promptPath) {
|
|
1015
|
+
const args = Array.isArray(invocation?.args) ? [...invocation.args] : [];
|
|
1016
|
+
const command = invocation.command === "codex" && invocation.capacityRecovery
|
|
1017
|
+
? ["node", "/tmp/citeark-codex-capacity-retry.mjs", String(invocation.capacityRecovery.retryDelayMs), String(invocation.capacityRecovery.maxRetries)]
|
|
1018
|
+
: [invocation.command];
|
|
1019
|
+
if (typeof invocation?.promptArgument === "string") {
|
|
1020
|
+
if (args.at(-1) !== invocation.promptArgument) {
|
|
1021
|
+
throw new TypeError("Agent invocation 的 promptArgument 必须是最后一个命令参数");
|
|
1022
|
+
}
|
|
1023
|
+
args.pop();
|
|
1024
|
+
const prefix = [...command, ...args].map(shellQuote).join(" ");
|
|
1025
|
+
return `${prefix} "$(cat ${shellQuote(promptPath)})"`;
|
|
1026
|
+
}
|
|
1027
|
+
return [...command, ...args].map(shellQuote).join(" ");
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function shellFunctions(executor) {
|
|
1031
|
+
const unavailableBoundary = `${executor}-supervisor-unavailable`;
|
|
1032
|
+
const separateUidBoundary = `${executor}-supervisor-separate-uid`;
|
|
1033
|
+
return `
|
|
1034
|
+
initialize_scientific_runtime() {
|
|
1035
|
+
if [ ! -x /opt/citeark/create-runtime-venv.sh ] || [ ! -x "$SUPERVISOR_PYTHON" ]; then
|
|
1036
|
+
printf '%s\n' "CiteArk 科学运行基线不完整,拒绝启动 Agent" >&2
|
|
1037
|
+
return 70
|
|
1038
|
+
fi
|
|
1039
|
+
PYTHON_PACKAGE_INDEX="\${CITEARK_PYTHON_PACKAGE_INDEX:-\${PIP_INDEX_URL:-https://pypi.org/simple}}"
|
|
1040
|
+
HUGGINGFACE_ENDPOINT="\${CITEARK_HUGGINGFACE_ENDPOINT:-\${HF_ENDPOINT:-https://huggingface.co}}"
|
|
1041
|
+
/opt/citeark/create-runtime-venv.sh "$RUNTIME_VIRTUAL_ENV"
|
|
1042
|
+
export VIRTUAL_ENV="$RUNTIME_VIRTUAL_ENV"
|
|
1043
|
+
export UV_PROJECT_ENVIRONMENT="$RUNTIME_VIRTUAL_ENV"
|
|
1044
|
+
export PATH="$RUNTIME_VIRTUAL_ENV/bin:/opt/citeark/venv/bin:/opt/citeark/cargo/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
|
1045
|
+
export PIP_INDEX_URL="$PYTHON_PACKAGE_INDEX"
|
|
1046
|
+
export UV_INDEX="$PYTHON_PACKAGE_INDEX"
|
|
1047
|
+
export UV_INDEX_STRATEGY=first-index
|
|
1048
|
+
export HF_ENDPOINT="$HUGGINGFACE_ENDPOINT"
|
|
1049
|
+
"$RUNTIME_VIRTUAL_ENV/bin/python" - <<'PY'
|
|
1050
|
+
import numpy
|
|
1051
|
+
import scipy
|
|
1052
|
+
import torch
|
|
1053
|
+
|
|
1054
|
+
assert torch.version.cuda == "12.8", torch.version.cuda
|
|
1055
|
+
print("CiteArk scientific task environment: ready")
|
|
1056
|
+
PY
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
configure_runner_boundary() {
|
|
1060
|
+
mkdir -p /job/execution
|
|
1061
|
+
if [ "$(id -u)" != 0 ] || ! id node >/dev/null 2>&1 || ! command -v runuser >/dev/null 2>&1; then
|
|
1062
|
+
jq -nc --arg captureBoundary ${shellQuote(unavailableBoundary)} '{schemaVersion:"0.1",$captureBoundary,agentCanWriteAuditDirectory:true}' > /job/execution/runner-boundary.json
|
|
1063
|
+
printf '%s\n' ${shellQuote(`${executor} runner 无法建立独立 UID 审计边界,拒绝启动 Agent`)} >&2
|
|
1064
|
+
return 1
|
|
1065
|
+
fi
|
|
1066
|
+
chmod 0711 /job
|
|
1067
|
+
chown -R node:node /job/workspace /job/output /job/runtime-home /job/assets /job/runtime-cache /job/runtime-venv
|
|
1068
|
+
chown root:node /job/execution
|
|
1069
|
+
chmod 0710 /job/execution
|
|
1070
|
+
if ! runuser -u node -- env HOME=/job/runtime-home UV_CONFIG_FILE=/dev/null /bin/bash -lc 'mkdir -p "$HOME/.local/share/opencode" && test -w "$HOME/.local/share/opencode" && test -w /job/assets && test -w /job/runtime-cache && test -w /job/runtime-venv && test -w /job/runtime-venv/lib/python3.12/site-packages'; then
|
|
1071
|
+
printf '%s\n' ${shellQuote(`${executor} runner 的低权限 Agent 无法访问可写运行目录,拒绝启动`)} >&2
|
|
1072
|
+
return 1
|
|
1073
|
+
fi
|
|
1074
|
+
AGENT_RUN_USER=node
|
|
1075
|
+
RUNNER_CAPTURE_BOUNDARY=${shellQuote(separateUidBoundary)}
|
|
1076
|
+
jq -nc --arg captureBoundary "$RUNNER_CAPTURE_BOUNDARY" --arg agentUser "\${AGENT_RUN_USER:-$(id -un)}" \\
|
|
1077
|
+
--arg separateUidBoundary ${shellQuote(separateUidBoundary)} \\
|
|
1078
|
+
'{schemaVersion:"0.1",$captureBoundary,$agentUser,runnerUser:(env.USER // "root"),agentCanWriteAuditDirectory:($captureBoundary != $separateUidBoundary)}' \\
|
|
1079
|
+
> /job/execution/runner-boundary.json
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
record_disk_usage() {
|
|
1083
|
+
local stage="$1" available_kb used_kb total_kb
|
|
1084
|
+
read -r total_kb used_kb available_kb < <(df -Pk /job | awk 'NR==2 {print $2, $3, $4}')
|
|
1085
|
+
jq -nc \
|
|
1086
|
+
--arg stage "$stage" \
|
|
1087
|
+
--arg recordedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
1088
|
+
--argjson totalBytes "$((total_kb * 1024))" \
|
|
1089
|
+
--argjson usedBytes "$((used_kb * 1024))" \
|
|
1090
|
+
--argjson availableBytes "$((available_kb * 1024))" \
|
|
1091
|
+
'{schemaVersion:"0.1",$stage,$recordedAt,$totalBytes,$usedBytes,$availableBytes}' \
|
|
1092
|
+
>> /job/execution/disk-usage.jsonl
|
|
1093
|
+
printf '%s\n' "$available_kb"
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
ensure_disk_headroom() {
|
|
1097
|
+
local minimum_gib="$1" stage="$2" available_kb
|
|
1098
|
+
available_kb="$(record_disk_usage "$stage")"
|
|
1099
|
+
[ "$available_kb" -ge "$((minimum_gib * 1024 * 1024))" ]
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
release_rebuildable_runtime_cache() {
|
|
1103
|
+
set +e
|
|
1104
|
+
find /job/runtime-cache /job/assets -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + 2>/dev/null
|
|
1105
|
+
rm -rf /job/runtime-venv /job/workspace/venv /job/workspace/runtime-venv \
|
|
1106
|
+
/job/workspace/repository/.venv /job/workspace/repository/venv \
|
|
1107
|
+
/job/runtime-home/.cache /job/runtime-home/.npm \
|
|
1108
|
+
/job/runtime-home/.local/share/uv /job/runtime-home/.local/share/pnpm \
|
|
1109
|
+
/job/runtime-home/.bun/install/cache /job/runtime-home/.config/opencode/node_modules
|
|
1110
|
+
record_disk_usage post-cache-release >/dev/null 2>&1 || true
|
|
1111
|
+
set -e
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
hydrate_provider_secret() {
|
|
1115
|
+
if [ -n "\${CITEARK_CODEX_ACCOUNT_ID:-}" ]; then
|
|
1116
|
+
unset OPENAI_API_KEY OPENAI_BASE_URL
|
|
1117
|
+
node /tmp/citeark-codex-account.mjs open
|
|
1118
|
+
return $?
|
|
1119
|
+
fi
|
|
1120
|
+
local credential_name="\${CITEARK_PROVIDER_SECRET_ENV_NAME:-}"
|
|
1121
|
+
local secret_resource="\${CITEARK_PROVIDER_SECRET_RESOURCE:-}"
|
|
1122
|
+
local credential_url="\${CITEARK_PROVIDER_CREDENTIAL_URL:-}"
|
|
1123
|
+
local credential_token="\${CITEARK_PROVIDER_CREDENTIAL_TOKEN:-}"
|
|
1124
|
+
local access_token encoded secret_value
|
|
1125
|
+
if [ -z "$credential_name" ]; then
|
|
1126
|
+
printf '%s\n' "运行时密钥环境变量名缺失" >&2
|
|
1127
|
+
return 1
|
|
1128
|
+
fi
|
|
1129
|
+
case "$credential_name" in
|
|
1130
|
+
ANTHROPIC_API_KEY|OPENAI_API_KEY|DEEPSEEK_API_KEY|CITEARK_API_KEY) ;;
|
|
1131
|
+
*) printf '%s\n' "运行时密钥环境变量名无效" >&2; return 1 ;;
|
|
1132
|
+
esac
|
|
1133
|
+
if [ -n "$(printenv "$credential_name" 2>/dev/null)" ]; then
|
|
1134
|
+
return 0
|
|
1135
|
+
fi
|
|
1136
|
+
if [ -n "$credential_url" ] || [ -n "$credential_token" ]; then
|
|
1137
|
+
if [ -z "$credential_url" ] || [ -z "$credential_token" ]; then
|
|
1138
|
+
printf '%s\n' "BYOK 凭据代理配置不完整" >&2
|
|
1139
|
+
return 1
|
|
1140
|
+
fi
|
|
1141
|
+
encoded="$(curl --fail --silent --show-error \\
|
|
1142
|
+
-H "Authorization: Bearer $credential_token" \\
|
|
1143
|
+
-H 'Accept: application/json' \\
|
|
1144
|
+
"$credential_url" \\
|
|
1145
|
+
| jq -er '.data')"
|
|
1146
|
+
elif [ -z "$secret_resource" ]; then
|
|
1147
|
+
printf '%s\n' "GCP Batch 运行时密钥引用不完整" >&2
|
|
1148
|
+
return 1
|
|
1149
|
+
else
|
|
1150
|
+
access_token="$(curl --fail --silent --show-error \\
|
|
1151
|
+
-H 'Metadata-Flavor: Google' \\
|
|
1152
|
+
'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token' \\
|
|
1153
|
+
| jq -er '.access_token')"
|
|
1154
|
+
encoded="$(curl --fail --silent --show-error \\
|
|
1155
|
+
-H "Authorization: Bearer $access_token" \\
|
|
1156
|
+
-H 'Content-Type: application/json' \\
|
|
1157
|
+
"https://secretmanager.googleapis.com/v1/$secret_resource:access" \\
|
|
1158
|
+
| jq -er '.payload.data')"
|
|
1159
|
+
fi
|
|
1160
|
+
secret_value="$(printf '%s' "$encoded" | base64 -d)"
|
|
1161
|
+
if [ -z "$secret_value" ]; then
|
|
1162
|
+
printf '%s\n' "GCP Batch 运行时密钥为空" >&2
|
|
1163
|
+
return 1
|
|
1164
|
+
fi
|
|
1165
|
+
export "$credential_name=$secret_value"
|
|
1166
|
+
unset access_token encoded secret_value credential_token credential_url
|
|
1167
|
+
unset CITEARK_PROVIDER_CREDENTIAL_TOKEN CITEARK_PROVIDER_CREDENTIAL_URL
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
start_provider_relay() {
|
|
1171
|
+
local count=0
|
|
1172
|
+
node --use-env-proxy "$PROVIDER_RELAY_PATH" >> /job/execution/provider-pro-relay.log 2>&1 &
|
|
1173
|
+
PROVIDER_RELAY_PID=$!
|
|
1174
|
+
while [ "$count" -lt 50 ]; do
|
|
1175
|
+
if curl --fail --silent --show-error "http://127.0.0.1:\${CITEARK_CODEX_RELAY_PORT:-4317}/health" >/dev/null 2>&1; then
|
|
1176
|
+
return 0
|
|
1177
|
+
fi
|
|
1178
|
+
kill -0 "$PROVIDER_RELAY_PID" 2>/dev/null || break
|
|
1179
|
+
sleep 0.1
|
|
1180
|
+
count=$((count + 1))
|
|
1181
|
+
done
|
|
1182
|
+
printf '%s\n' "Provider Pro relay 启动失败" >&2
|
|
1183
|
+
return 1
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
stop_provider_relay() {
|
|
1187
|
+
if [ -n "\${PROVIDER_RELAY_PID:-}" ] && kill -0 "$PROVIDER_RELAY_PID" 2>/dev/null; then
|
|
1188
|
+
kill -TERM "$PROVIDER_RELAY_PID" 2>/dev/null || true
|
|
1189
|
+
wait "$PROVIDER_RELAY_PID" 2>/dev/null || true
|
|
1190
|
+
fi
|
|
1191
|
+
PROVIDER_RELAY_PID=
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
runtime_budget_consumed_seconds() {
|
|
1195
|
+
local now elapsed consumed
|
|
1196
|
+
if [ "$TOTAL_RUNTIME_BUDGET_SECONDS" -le 0 ]; then
|
|
1197
|
+
printf '0\n'
|
|
1198
|
+
return 0
|
|
1199
|
+
fi
|
|
1200
|
+
now="$(date +%s)"
|
|
1201
|
+
elapsed=$((now - RUNTIME_BUDGET_SEGMENT_STARTED_EPOCH))
|
|
1202
|
+
[ "$elapsed" -ge 0 ] || elapsed=0
|
|
1203
|
+
consumed=$((RUNTIME_BUDGET_BASE_SECONDS + elapsed))
|
|
1204
|
+
if [ "$consumed" -gt "$TOTAL_RUNTIME_BUDGET_SECONDS" ]; then consumed="$TOTAL_RUNTIME_BUDGET_SECONDS"; fi
|
|
1205
|
+
printf '%s\n' "$consumed"
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
write_runtime_budget() {
|
|
1209
|
+
local consumed temporary
|
|
1210
|
+
[ "$TOTAL_RUNTIME_BUDGET_SECONDS" -gt 0 ] || return 0
|
|
1211
|
+
consumed="$(runtime_budget_consumed_seconds)"
|
|
1212
|
+
temporary="\${RUNTIME_BUDGET_STATE}.tmp"
|
|
1213
|
+
jq -nc \
|
|
1214
|
+
--argjson totalRuntimeBudgetSeconds "$TOTAL_RUNTIME_BUDGET_SECONDS" \
|
|
1215
|
+
--argjson consumedRuntimeSeconds "$consumed" \
|
|
1216
|
+
--arg updatedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
1217
|
+
'{schemaVersion:"0.1",$totalRuntimeBudgetSeconds,$consumedRuntimeSeconds,$updatedAt}' \
|
|
1218
|
+
> "$temporary"
|
|
1219
|
+
mv "$temporary" "$RUNTIME_BUDGET_STATE"
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
initialize_runtime_budget() {
|
|
1223
|
+
local restored=0
|
|
1224
|
+
if [ "$TOTAL_RUNTIME_BUDGET_SECONDS" -le 0 ]; then return 0; fi
|
|
1225
|
+
if [ -s "$RUNTIME_BUDGET_STATE" ]; then
|
|
1226
|
+
restored="$(jq -r '.consumedRuntimeSeconds // 0' "$RUNTIME_BUDGET_STATE" 2>/dev/null || printf '0')"
|
|
1227
|
+
fi
|
|
1228
|
+
case "$restored" in ''|*[!0-9]*) restored=0 ;; esac
|
|
1229
|
+
if [ "$restored" -gt "$TOTAL_RUNTIME_BUDGET_SECONDS" ]; then restored="$TOTAL_RUNTIME_BUDGET_SECONDS"; fi
|
|
1230
|
+
RUNTIME_BUDGET_BASE_SECONDS="$restored"
|
|
1231
|
+
RUNTIME_BUDGET_SEGMENT_STARTED_EPOCH="$(date +%s)"
|
|
1232
|
+
write_runtime_budget
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
remaining_runtime_budget_seconds() {
|
|
1236
|
+
local consumed remaining
|
|
1237
|
+
if [ "$TOTAL_RUNTIME_BUDGET_SECONDS" -le 0 ]; then
|
|
1238
|
+
printf '0\n'
|
|
1239
|
+
return 0
|
|
1240
|
+
fi
|
|
1241
|
+
consumed="$(runtime_budget_consumed_seconds)"
|
|
1242
|
+
remaining=$((TOTAL_RUNTIME_BUDGET_SECONDS - consumed))
|
|
1243
|
+
[ "$remaining" -ge 0 ] || remaining=0
|
|
1244
|
+
printf '%s\n' "$remaining"
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
latest_checkpoint() {
|
|
1248
|
+
local kind="$1"
|
|
1249
|
+
ls -1t "\${RESTORE_CHECKPOINT_PREFIX}-\${kind}-"*.tar.gz 2>/dev/null | head -n 1 || true
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
restore_checkpoints() {
|
|
1253
|
+
local full critical full_mtime=0 critical_mtime=0
|
|
1254
|
+
full="$(latest_checkpoint full)"
|
|
1255
|
+
critical="$(latest_checkpoint critical)"
|
|
1256
|
+
if [ -n "$full" ]; then
|
|
1257
|
+
verify_checkpoint_archive "$full"
|
|
1258
|
+
tar -xzf "$full" -C /job
|
|
1259
|
+
full_mtime="$(stat -c %Y "$full" 2>/dev/null || echo 0)"
|
|
1260
|
+
RESTORED_CHECKPOINT=1
|
|
1261
|
+
fi
|
|
1262
|
+
if [ -n "$critical" ]; then
|
|
1263
|
+
critical_mtime="$(stat -c %Y "$critical" 2>/dev/null || echo 0)"
|
|
1264
|
+
if [ "$critical_mtime" -ge "$full_mtime" ]; then
|
|
1265
|
+
verify_checkpoint_archive "$critical"
|
|
1266
|
+
tar -xzf "$critical" -C /job
|
|
1267
|
+
RESTORED_CHECKPOINT=1
|
|
1268
|
+
fi
|
|
1269
|
+
fi
|
|
1270
|
+
if [ "$RESTORED_CHECKPOINT" = 1 ]; then
|
|
1271
|
+
if [ -d /job/execution/workspace-checkpoint ]; then restore_workspace_checkpoint; fi
|
|
1272
|
+
mkdir -p /job/execution
|
|
1273
|
+
printf '{"type":"citeark.checkpoint_restored","infrastructureRetry":%s,"full":"%s","critical":"%s","timestamp":"%s"}\n' \
|
|
1274
|
+
"$INFRASTRUCTURE_RETRY" "$(basename "\${full:-}")" "$(basename "\${critical:-}")" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> /job/execution/trace.jsonl
|
|
1275
|
+
fi
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
verify_checkpoint_archive() {
|
|
1279
|
+
local archive="$1" status="\${RESTORE_CHECKPOINT_PREFIX}-status.json"
|
|
1280
|
+
local object expected actual cache_required=0
|
|
1281
|
+
case "$RESTORE_CHECKPOINT_PREFIX" in *citeark-asset-cache-manifest-*) cache_required=1 ;; esac
|
|
1282
|
+
if [ ! -s "$status" ]; then
|
|
1283
|
+
if [ "$cache_required" = 1 ]; then
|
|
1284
|
+
printf '%s\n' "资产缓存缺少检查点状态文件" >&2
|
|
1285
|
+
return 1
|
|
1286
|
+
fi
|
|
1287
|
+
return 0
|
|
1288
|
+
fi
|
|
1289
|
+
object="$(jq -r '.object // empty' "$status" 2>/dev/null || true)"
|
|
1290
|
+
if [ "$object" != "$(basename "$archive")" ]; then
|
|
1291
|
+
if [ "$cache_required" = 1 ]; then
|
|
1292
|
+
printf '%s\n' "资产缓存状态没有绑定待恢复检查点" >&2
|
|
1293
|
+
return 1
|
|
1294
|
+
fi
|
|
1295
|
+
return 0
|
|
1296
|
+
fi
|
|
1297
|
+
expected="$(jq -r '.sha256 // empty' "$status" 2>/dev/null || true)"
|
|
1298
|
+
case "$expected" in sha256:[0-9a-f][0-9a-f]*) ;; *)
|
|
1299
|
+
if [ "$cache_required" = 1 ]; then
|
|
1300
|
+
printf '%s\n' "资产缓存检查点缺少 SHA-256" >&2
|
|
1301
|
+
return 1
|
|
1302
|
+
fi
|
|
1303
|
+
return 0
|
|
1304
|
+
esac
|
|
1305
|
+
actual="sha256:$(sha256sum "$archive" | cut -d' ' -f1)"
|
|
1306
|
+
if [ "$actual" != "$expected" ]; then
|
|
1307
|
+
printf '%s\n' "检查点 SHA-256 不匹配,拒绝恢复" >&2
|
|
1308
|
+
return 1
|
|
1309
|
+
fi
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
capture_workspace_checkpoint() {
|
|
1313
|
+
CITEARK_JOB_ROOT=/job "$SUPERVISOR_PYTHON" -c ${shellQuote(WORKSPACE_CHECKPOINT_CAPTURE_PYTHON)}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
restore_workspace_checkpoint() {
|
|
1317
|
+
CITEARK_JOB_ROOT=/job "$SUPERVISOR_PYTHON" -c ${shellQuote(WORKSPACE_CHECKPOINT_RESTORE_PYTHON)}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
flush_checkpoint_object() {
|
|
1321
|
+
local path="$1" expected_bytes="\${2:-}" actual_bytes
|
|
1322
|
+
if ! sync -f "$path" 2>> /job/execution/checkpoint.log; then
|
|
1323
|
+
sync 2>> /job/execution/checkpoint.log || return 1
|
|
1324
|
+
fi
|
|
1325
|
+
if [ ! -s "$path" ]; then
|
|
1326
|
+
printf '%s\n' "检查点对象落盘后为空:$path" >> /job/execution/checkpoint.log
|
|
1327
|
+
return 1
|
|
1328
|
+
fi
|
|
1329
|
+
actual_bytes="$(stat -c %s "$path" 2>/dev/null || true)"
|
|
1330
|
+
if [ -n "$expected_bytes" ] && [ "$actual_bytes" != "$expected_bytes" ]; then
|
|
1331
|
+
printf '%s\n' "检查点对象落盘后大小不一致:$path expected=$expected_bytes actual=$actual_bytes" >> /job/execution/checkpoint.log
|
|
1332
|
+
return 1
|
|
1333
|
+
fi
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
checkpoint_state() {
|
|
1337
|
+
local kind="$1" reason="\${2:-periodic}" sequence temporary target status_tmp status_bytes bytes created archive_sha256 paths=()
|
|
1338
|
+
local trace_tail trace_digest retention_excludes last_effective_epoch last_effective_at
|
|
1339
|
+
[ "$CHECKPOINT_ENABLED" = 1 ] || return 0
|
|
1340
|
+
mkdir "$CHECKPOINT_LOCK" 2>/dev/null || return 0
|
|
1341
|
+
write_runtime_budget || true
|
|
1342
|
+
if ! node /tmp/citeark-codex-account.mjs save; then rmdir "$CHECKPOINT_LOCK"; return 1; fi
|
|
1343
|
+
if ! capture_workspace_checkpoint 2>> /job/execution/checkpoint.log; then
|
|
1344
|
+
rmdir "$CHECKPOINT_LOCK" 2>/dev/null || true
|
|
1345
|
+
return 1
|
|
1346
|
+
fi
|
|
1347
|
+
sequence="$(date -u +%Y%m%dT%H%M%S)-$(date +%N)-r\${INFRASTRUCTURE_RETRY}"
|
|
1348
|
+
temporary="/tmp/citeark-checkpoint-\${kind}-\${sequence}.tar.gz"
|
|
1349
|
+
retention_excludes="/tmp/citeark-checkpoint-excludes-\${sequence}.txt"
|
|
1350
|
+
target="\${CHECKPOINT_PREFIX}-\${kind}-\${sequence}.tar.gz"
|
|
1351
|
+
: > "$retention_excludes"
|
|
1352
|
+
cat >> "$retention_excludes" <<'CHECKPOINT_EXCLUDES'
|
|
1353
|
+
runtime-home/codex/auth.json
|
|
1354
|
+
runtime-home/.cache
|
|
1355
|
+
runtime-home/.npm
|
|
1356
|
+
runtime-home/.local/share/uv
|
|
1357
|
+
runtime-home/.local/share/pnpm
|
|
1358
|
+
runtime-home/.bun/install/cache
|
|
1359
|
+
runtime-home/.config/opencode/node_modules
|
|
1360
|
+
runtime-home/.local/share/opencode/log
|
|
1361
|
+
runtime-home/.local/share/opencode/snapshot
|
|
1362
|
+
runtime-home/.local/share/opencode/repos
|
|
1363
|
+
execution/.publication-reserve
|
|
1364
|
+
output/.env*
|
|
1365
|
+
output/*/.env*
|
|
1366
|
+
output/auth.json
|
|
1367
|
+
output/*/auth.json
|
|
1368
|
+
output/credentials.json
|
|
1369
|
+
output/*/credentials.json
|
|
1370
|
+
output/secrets.json
|
|
1371
|
+
output/*/secrets.json
|
|
1372
|
+
workspace/repository/.git
|
|
1373
|
+
workspace/repository/*/.git
|
|
1374
|
+
workspace/repository/.cache
|
|
1375
|
+
workspace/repository/*/.cache
|
|
1376
|
+
workspace/repository/.mypy_cache
|
|
1377
|
+
workspace/repository/*/.mypy_cache
|
|
1378
|
+
workspace/repository/.pytest_cache
|
|
1379
|
+
workspace/repository/*/.pytest_cache
|
|
1380
|
+
workspace/repository/.venv
|
|
1381
|
+
workspace/repository/*/.venv
|
|
1382
|
+
workspace/repository/__pycache__
|
|
1383
|
+
workspace/repository/*/__pycache__
|
|
1384
|
+
workspace/repository/node_modules
|
|
1385
|
+
workspace/repository/*/node_modules
|
|
1386
|
+
workspace/repository/.env
|
|
1387
|
+
workspace/repository/.env.*
|
|
1388
|
+
workspace/repository/*/.env
|
|
1389
|
+
workspace/repository/*/.env.*
|
|
1390
|
+
CHECKPOINT_EXCLUDES
|
|
1391
|
+
if [ -s "$COMPLETION" ] && jq -e . "$COMPLETION" >/dev/null 2>&1; then
|
|
1392
|
+
while IFS= read -r relative; do
|
|
1393
|
+
case "$relative" in
|
|
1394
|
+
""|/*|../*|*/../*|*/..) continue ;;
|
|
1395
|
+
esac
|
|
1396
|
+
printf 'output/%s\n' "$relative" >> "$retention_excludes"
|
|
1397
|
+
done < <(jq -r '.outputs[]? | select(.kind == "model" or .kind == "checkpoint" or .kind == "dataset") | .path // empty' "$COMPLETION")
|
|
1398
|
+
fi
|
|
1399
|
+
if [ "$reason" != asset-preparation ]; then
|
|
1400
|
+
[ -d /job/execution ] && paths+=(execution)
|
|
1401
|
+
[ -d /job/runtime-home ] && paths+=(runtime-home)
|
|
1402
|
+
fi
|
|
1403
|
+
# Preserve the full scientific output tree, including raw histories.
|
|
1404
|
+
[ -d /job/output ] && paths+=(output)
|
|
1405
|
+
if [ "$reason" = target-recovery ] || [ "$reason" = asset-preparation ] || [ "$PRESERVE_REBUILDABLE_CACHE" = 1 ]; then
|
|
1406
|
+
[ -d /job/assets ] && paths+=(assets)
|
|
1407
|
+
[ -d /job/runtime-cache ] && paths+=(runtime-cache)
|
|
1408
|
+
[ -d /job/runtime-venv ] && paths+=(runtime-venv)
|
|
1409
|
+
fi
|
|
1410
|
+
if [ "\${#paths[@]}" -eq 0 ]; then rm -f "$retention_excludes"; rmdir "$CHECKPOINT_LOCK"; return 0; fi
|
|
1411
|
+
if tar --ignore-failed-read --warning=no-file-changed --exclude-from="$retention_excludes" -I 'gzip -1' -cf "$temporary" -C /job "\${paths[@]}" 2>> /job/execution/checkpoint.log; then
|
|
1412
|
+
bytes="$(stat -c %s "$temporary")"
|
|
1413
|
+
cp "$temporary" "$target"
|
|
1414
|
+
if ! flush_checkpoint_object "$target" "$bytes"; then
|
|
1415
|
+
rm -f "$target" "$temporary" "$retention_excludes"
|
|
1416
|
+
rmdir "$CHECKPOINT_LOCK" 2>/dev/null || true
|
|
1417
|
+
return 1
|
|
1418
|
+
fi
|
|
1419
|
+
archive_sha256="sha256:$(sha256sum "$temporary" | cut -d' ' -f1)"
|
|
1420
|
+
created="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
1421
|
+
status_tmp="/tmp/citeark-checkpoint-status-\${sequence}.json"
|
|
1422
|
+
trace_tail="$(grep -v '"type":"citeark.checkpoint_saved"' /job/execution/trace.jsonl 2>/dev/null \
|
|
1423
|
+
| tail -n 80 \
|
|
1424
|
+
| jq -Rsc '
|
|
1425
|
+
def clean($n):
|
|
1426
|
+
if type == "string" then
|
|
1427
|
+
gsub("sk-[A-Za-z0-9_-]{12,}"; "<redacted-secret>")
|
|
1428
|
+
| if length > $n then .[0:($n - 1)] + "…" else . end
|
|
1429
|
+
else null end;
|
|
1430
|
+
def compact: with_entries(select(.value != null));
|
|
1431
|
+
[
|
|
1432
|
+
splits("\\n")
|
|
1433
|
+
| fromjson?
|
|
1434
|
+
| select(type == "object")
|
|
1435
|
+
| {
|
|
1436
|
+
type,
|
|
1437
|
+
timestamp: (.timestamp | clean(64)),
|
|
1438
|
+
recorded_at: (.recorded_at | clean(64)),
|
|
1439
|
+
created_at: (.created_at | clean(64)),
|
|
1440
|
+
attempt,
|
|
1441
|
+
executor: (.executor | clean(64)),
|
|
1442
|
+
infrastructureRetry,
|
|
1443
|
+
restoredCheckpoint,
|
|
1444
|
+
kind: (.kind | clean(48)),
|
|
1445
|
+
reason: (.reason | clean(120)),
|
|
1446
|
+
sequence: (.sequence | clean(120)),
|
|
1447
|
+
bytes,
|
|
1448
|
+
exitCode,
|
|
1449
|
+
exit_code,
|
|
1450
|
+
timed_out,
|
|
1451
|
+
resultSha256: (.resultSha256 | clean(80)),
|
|
1452
|
+
signal: (.signal | clean(32)),
|
|
1453
|
+
thread_id: (.thread_id | clean(120)),
|
|
1454
|
+
turn_id: (.turn_id | clean(120)),
|
|
1455
|
+
is_error,
|
|
1456
|
+
tool_name: (.tool_name | clean(80)),
|
|
1457
|
+
elapsed_time_seconds,
|
|
1458
|
+
item: (
|
|
1459
|
+
if (.item | type) == "object" then
|
|
1460
|
+
{
|
|
1461
|
+
id: (.item.id | clean(120)),
|
|
1462
|
+
type: (.item.type | clean(80)),
|
|
1463
|
+
status: (.item.status | clean(48)),
|
|
1464
|
+
exit_code: .item.exit_code,
|
|
1465
|
+
command: (.item.command | clean(300)),
|
|
1466
|
+
text: (.item.text | clean(500))
|
|
1467
|
+
} | compact
|
|
1468
|
+
else null end
|
|
1469
|
+
)
|
|
1470
|
+
} | compact
|
|
1471
|
+
] | .[-24:]' 2>/dev/null \
|
|
1472
|
+
|| printf '[]')"
|
|
1473
|
+
trace_digest="$(printf '%s' "$trace_tail" | sha256sum | cut -d' ' -f1)"
|
|
1474
|
+
last_effective_epoch="$(agent_last_effective_activity_epoch)"
|
|
1475
|
+
last_effective_at=""
|
|
1476
|
+
if [ "$last_effective_epoch" -gt 0 ]; then
|
|
1477
|
+
last_effective_at="$(date -u -d "@$last_effective_epoch" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || true)"
|
|
1478
|
+
fi
|
|
1479
|
+
jq -nc \
|
|
1480
|
+
--arg sequence "$sequence" \
|
|
1481
|
+
--arg kind "$kind" \
|
|
1482
|
+
--arg reason "$reason" \
|
|
1483
|
+
--arg object "$(basename "$target")" \
|
|
1484
|
+
--arg createdAt "$created" \
|
|
1485
|
+
--argjson bytes "$bytes" \
|
|
1486
|
+
--arg sha256 "$archive_sha256" \
|
|
1487
|
+
--argjson infrastructureRetry "$INFRASTRUCTURE_RETRY" \
|
|
1488
|
+
--arg traceDigest "$trace_digest" \
|
|
1489
|
+
--argjson traceTail "$trace_tail" \
|
|
1490
|
+
--arg lastEffectiveAt "$last_effective_at" \
|
|
1491
|
+
'{schemaVersion:"0.1",$sequence,$kind,$reason,$object,$createdAt,$bytes,$sha256,$infrastructureRetry,activity:({traceDigest:$traceDigest,traceTail:$traceTail,observedAt:$createdAt} + (if $lastEffectiveAt == "" then {} else {lastEffectiveAt:$lastEffectiveAt} end))}' \
|
|
1492
|
+
> "$status_tmp"
|
|
1493
|
+
status_bytes="$(stat -c %s "$status_tmp")"
|
|
1494
|
+
cp "$status_tmp" "$CHECKPOINT_STATUS"
|
|
1495
|
+
if ! flush_checkpoint_object "$CHECKPOINT_STATUS" "$status_bytes" || ! jq -e . "$CHECKPOINT_STATUS" >/dev/null 2>&1; then
|
|
1496
|
+
printf '%s\n' "检查点状态文件未可靠落盘,拒绝发布检查点:$CHECKPOINT_STATUS" >> /job/execution/checkpoint.log
|
|
1497
|
+
rm -f "$CHECKPOINT_STATUS" "$status_tmp" "$target" "$temporary" "$retention_excludes"
|
|
1498
|
+
rmdir "$CHECKPOINT_LOCK" 2>/dev/null || true
|
|
1499
|
+
return 1
|
|
1500
|
+
fi
|
|
1501
|
+
printf '{"type":"citeark.checkpoint_saved","kind":"%s","reason":"%s","sequence":"%s","bytes":%s,"timestamp":"%s"}\n' \
|
|
1502
|
+
"$kind" "$reason" "$sequence" "$bytes" "$created" >> /job/execution/trace.jsonl
|
|
1503
|
+
ls -1t "\${CHECKPOINT_PREFIX}-\${kind}-"*.tar.gz 2>/dev/null | tail -n +$((CHECKPOINT_RETENTION + 1)) | while IFS= read -r old; do rm -f "$old"; done
|
|
1504
|
+
rm -f "$status_tmp"
|
|
1505
|
+
fi
|
|
1506
|
+
rm -f "$temporary" "$retention_excludes"
|
|
1507
|
+
rmdir "$CHECKPOINT_LOCK" 2>/dev/null || true
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
checkpoint_signature() {
|
|
1511
|
+
find /job/execution /job/runtime-home /job/workspace/repository /job/output/evidence /job/output/logs \
|
|
1512
|
+
\\( -path '*/.git' -o -path '*/.cache' -o -path '*/.mypy_cache' -o -path '*/.pytest_cache' \
|
|
1513
|
+
-o -path '*/.venv' -o -path '*/__pycache__' -o -path '*/node_modules' \
|
|
1514
|
+
-o -path '/job/runtime-home/.npm' -o -path '/job/runtime-home/.local/share/uv' \
|
|
1515
|
+
-o -path '/job/runtime-home/.local/share/pnpm' -o -path '/job/runtime-home/.bun/install/cache' \
|
|
1516
|
+
-o -path '/job/runtime-home/.config/opencode/node_modules' \
|
|
1517
|
+
-o -path '/job/runtime-home/.local/share/opencode/log' \
|
|
1518
|
+
-o -path '/job/runtime-home/.local/share/opencode/snapshot' \
|
|
1519
|
+
-o -path '/job/runtime-home/.local/share/opencode/repos' \\) -prune \
|
|
1520
|
+
-o -type f ! -name '.env' ! -name '.env.*' \
|
|
1521
|
+
! -path '/job/execution/trace.jsonl' ! -path '/job/execution/checkpoint.log' \
|
|
1522
|
+
! -path '/job/execution/runtime-budget.json' \
|
|
1523
|
+
! -path '/job/execution/.publication-reserve' \
|
|
1524
|
+
! -path '/job/execution/workspace-checkpoint/*' -printf '%T@:%s:%p\n' 2>/dev/null \
|
|
1525
|
+
| sort | tail -n 32 | sha256sum | cut -d' ' -f1
|
|
1526
|
+
if [ -f /job/execution/trace.jsonl ]; then
|
|
1527
|
+
grep -v '"type":"citeark.checkpoint_saved"' /job/execution/trace.jsonl 2>/dev/null \
|
|
1528
|
+
| tail -n 80 | sha256sum | cut -d' ' -f1
|
|
1529
|
+
fi
|
|
1530
|
+
if [ -f "$COMPLETION" ]; then stat -c '%Y:%s' "$COMPLETION"; fi
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
agent_last_effective_activity_epoch() {
|
|
1534
|
+
local value
|
|
1535
|
+
value="$(grep -v -E '"type":"citeark\.(checkpoint_saved|agent_stalled)"' /job/execution/trace.jsonl 2>/dev/null \
|
|
1536
|
+
| tail -n 1 \
|
|
1537
|
+
| jq -r '.timestamp // .recorded_at // .recordedAt // .created_at // .createdAt // empty' 2>/dev/null \
|
|
1538
|
+
|| true)"
|
|
1539
|
+
case "$value" in
|
|
1540
|
+
""|null) printf '0\n' ;;
|
|
1541
|
+
*[!0-9]*) date -u -d "$value" +%s 2>/dev/null || printf '0\n' ;;
|
|
1542
|
+
*)
|
|
1543
|
+
if [ "\${#value}" -ge 13 ]; then printf '%s\n' "$((value / 1000))"; else printf '%s\n' "$value"; fi
|
|
1544
|
+
;;
|
|
1545
|
+
esac
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
mark_result_ready() {
|
|
1549
|
+
local reason="\${1:-agent-completed}" evidence digest ready_tmp
|
|
1550
|
+
[ "$AGENT_EXIT" -eq 0 ] && [ -s "$COMPLETION" ] || return 1
|
|
1551
|
+
jq -e . "$COMPLETION" >/dev/null 2>&1 || return 1
|
|
1552
|
+
if [ "$COMPLETION_HAS_EVIDENCE" = 1 ]; then
|
|
1553
|
+
while IFS= read -r evidence; do
|
|
1554
|
+
case "$evidence" in
|
|
1555
|
+
""|/*|../*|*/../*|*/..) return 1 ;;
|
|
1556
|
+
esac
|
|
1557
|
+
[ -f "/job/output/$evidence" ] || return 1
|
|
1558
|
+
done < <(jq -r '.evidence[]?.path // empty' "$COMPLETION")
|
|
1559
|
+
fi
|
|
1560
|
+
digest="$(sha256sum "$COMPLETION" | cut -d' ' -f1)"
|
|
1561
|
+
ready_tmp="/tmp/citeark-result-ready.json"
|
|
1562
|
+
printf '{"schemaVersion":"0.1","resultSha256":"%s","reason":"%s","createdAt":"%s","infrastructureRetry":%s}\n' \
|
|
1563
|
+
"$digest" "$reason" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$INFRASTRUCTURE_RETRY" > "$ready_tmp"
|
|
1564
|
+
cp "$ready_tmp" "$RESULT_READY"
|
|
1565
|
+
printf '{"type":"citeark.result_ready","reason":"%s","resultSha256":"%s","timestamp":"%s"}\n' \
|
|
1566
|
+
"$reason" "$digest" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> /job/execution/trace.jsonl
|
|
1567
|
+
rm -f "$ready_tmp"
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
result_ready_matches() {
|
|
1571
|
+
local expected observed
|
|
1572
|
+
[ -s "$RESULT_READY" ] && [ -s "$COMPLETION" ] || return 1
|
|
1573
|
+
expected="$(jq -r 'select(.reason == "agent-completed") | .resultSha256 // empty' "$RESULT_READY" 2>/dev/null)"
|
|
1574
|
+
observed="$(sha256sum "$COMPLETION" | cut -d' ' -f1)"
|
|
1575
|
+
[ -n "$expected" ] && [ "$expected" = "$observed" ]
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
checkpoint_loop() {
|
|
1579
|
+
local last_full now signature last_signature=""
|
|
1580
|
+
local agent_pid="" last_effective_epoch=0 idle_seconds=0 last_effective_at=""
|
|
1581
|
+
last_full="$(date +%s)"
|
|
1582
|
+
checkpoint_state critical worker-started || true
|
|
1583
|
+
while [ ! -e "$CHECKPOINT_STOP" ]; do
|
|
1584
|
+
sleep "$CHECKPOINT_CRITICAL_INTERVAL" & wait $! || true
|
|
1585
|
+
[ -e "$CHECKPOINT_STOP" ] && break
|
|
1586
|
+
signature="$(checkpoint_signature)"
|
|
1587
|
+
if [ "$signature" != "$last_signature" ]; then
|
|
1588
|
+
checkpoint_state critical changed || true
|
|
1589
|
+
last_signature="$signature"
|
|
1590
|
+
fi
|
|
1591
|
+
# An environment checkpoint can take minutes. If the Agent finished while
|
|
1592
|
+
# it was being written, let the parent validate and publish the final one.
|
|
1593
|
+
[ -e "$CHECKPOINT_STOP" ] && break
|
|
1594
|
+
now="$(date +%s)"
|
|
1595
|
+
if [ "$AGENT_STALL_TIMEOUT_SECONDS" -gt 0 ] && [ ! -s "$AGENT_STALL_STATE" ]; then
|
|
1596
|
+
agent_pid="$(cat "$AGENT_PID_FILE" 2>/dev/null || true)"
|
|
1597
|
+
last_effective_epoch="$(agent_last_effective_activity_epoch)"
|
|
1598
|
+
if [ -n "$agent_pid" ] && kill -0 "$agent_pid" 2>/dev/null && [ "$last_effective_epoch" -gt 0 ]; then
|
|
1599
|
+
idle_seconds=$((now - last_effective_epoch))
|
|
1600
|
+
if [ "$idle_seconds" -ge "$AGENT_STALL_TIMEOUT_SECONDS" ]; then
|
|
1601
|
+
last_effective_at="$(date -u -d "@$last_effective_epoch" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || true)"
|
|
1602
|
+
jq -nc \
|
|
1603
|
+
--arg detectedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
1604
|
+
--arg lastEffectiveAt "$last_effective_at" \
|
|
1605
|
+
--argjson idleSeconds "$idle_seconds" \
|
|
1606
|
+
--argjson timeoutSeconds "$AGENT_STALL_TIMEOUT_SECONDS" \
|
|
1607
|
+
'{schemaVersion:"0.1",reason:"effective-progress-stalled",$detectedAt,$lastEffectiveAt,$idleSeconds,$timeoutSeconds}' \
|
|
1608
|
+
> "$AGENT_STALL_STATE"
|
|
1609
|
+
printf '{"type":"citeark.agent_stalled","reason":"effective-progress-stalled","idleSeconds":%s,"timeoutSeconds":%s,"lastEffectiveAt":"%s","timestamp":"%s"}\n' \
|
|
1610
|
+
"$idle_seconds" "$AGENT_STALL_TIMEOUT_SECONDS" "$last_effective_at" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> /job/execution/trace.jsonl
|
|
1611
|
+
checkpoint_state critical agent-stalled || true
|
|
1612
|
+
kill -TERM "$agent_pid" 2>/dev/null || true
|
|
1613
|
+
fi
|
|
1614
|
+
fi
|
|
1615
|
+
fi
|
|
1616
|
+
[ -e "$CHECKPOINT_STOP" ] && break
|
|
1617
|
+
if [ $((now - last_full)) -ge "$CHECKPOINT_FULL_INTERVAL" ]; then
|
|
1618
|
+
checkpoint_state full periodic || true
|
|
1619
|
+
last_full="$now"
|
|
1620
|
+
fi
|
|
1621
|
+
done
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
handle_termination() {
|
|
1625
|
+
set +e
|
|
1626
|
+
printf '{"type":"citeark.infrastructure_interruption","signal":"TERM","infrastructureRetry":%s,"timestamp":"%s"}\n' \
|
|
1627
|
+
"$INFRASTRUCTURE_RETRY" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> /job/execution/trace.jsonl
|
|
1628
|
+
checkpoint_state critical signal || true
|
|
1629
|
+
if [ -n "\${AGENT_PID:-}" ]; then kill -TERM "$AGENT_PID" 2>/dev/null || true; fi
|
|
1630
|
+
stop_provider_relay
|
|
1631
|
+
exit 143
|
|
1632
|
+
}`;
|
|
1633
|
+
}
|