@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.
Files changed (347) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +128 -0
  3. package/data/dataset-source-registry.v1.json +300 -0
  4. package/dist/arkgraph/boot.js +6 -0
  5. package/dist/arkgraph/index.html +1 -0
  6. package/dist/arkgraph/viewer.css +1 -0
  7. package/dist/arkgraph/viewer.en.css +1 -0
  8. package/dist/arkgraph/viewer.en.js +49 -0
  9. package/dist/arkgraph/viewer.en.js.LEGAL.txt +56 -0
  10. package/dist/arkgraph/viewer.js +49 -0
  11. package/dist/arkgraph/viewer.js.LEGAL.txt +56 -0
  12. package/docker/claude-code/Dockerfile +97 -0
  13. package/docker/claude-code/codex-pro-relay.mjs +466 -0
  14. package/docker/claude-code/runtime-contract-check.mjs +79 -0
  15. package/docs/arkgraph-reading.md +79 -0
  16. package/docs/configuration.md +100 -0
  17. package/docs/integration.md +92 -0
  18. package/docs/maturity-plan.md +27 -0
  19. package/docs/npm-release.md +44 -0
  20. package/docs/paper-reading.md +40 -0
  21. package/docs/research-plan-granularity.md +27 -0
  22. package/docs/terminal.md +49 -0
  23. package/examples/toy-evaluation/compile-task.json +27 -0
  24. package/examples/toy-evaluation/paper.md +5 -0
  25. package/examples/toy-evaluation/repository/README.md +9 -0
  26. package/examples/toy-evaluation/repository/checkpoint.json +4 -0
  27. package/examples/toy-evaluation/repository/evaluate.py +17 -0
  28. package/examples/toy-evaluation/task.json +81 -0
  29. package/package.json +59 -0
  30. package/prompts/compile-research.md +58 -0
  31. package/prompts/execute-contract.md +72 -0
  32. package/prompts/execute-workspace-simple.md +51 -0
  33. package/prompts/execute-workspace.md +34 -0
  34. package/prompts/prepare-reproduction.md +82 -0
  35. package/prompts/repair-research.md +45 -0
  36. package/protocol/CAP.md +129 -0
  37. package/protocol/LICENSE +12 -0
  38. package/protocol/MAPPINGS.md +72 -0
  39. package/protocol/README.md +38 -0
  40. package/protocol/conformance-v2.0-alpha.1.json +36 -0
  41. package/protocol/examples/arkgraph/checkpoint-evaluation.json +309 -0
  42. package/protocol/examples/arkgraph/fixtures.mjs +49 -0
  43. package/protocol/examples/arkgraph/paper-free.json +291 -0
  44. package/protocol/examples/arkgraph/partial-failure.json +344 -0
  45. package/protocol/examples/arkgraph/training-evaluation.json +443 -0
  46. package/protocol/profiles/agent-trace.md +16 -0
  47. package/protocol/profiles/computational-run.md +16 -0
  48. package/protocol/profiles/core.md +15 -0
  49. package/protocol/profiles/public-bundle.md +18 -0
  50. package/protocol/profiles/reproduction.md +29 -0
  51. package/protocol/profiles/research-compilation.md +44 -0
  52. package/protocol/profiles/research-plan.md +39 -0
  53. package/protocol/profiles/restricted-evidence.md +15 -0
  54. package/runtime/bootstrap-autodl-runtime.sh +314 -0
  55. package/runtime/create-runtime-venv.sh +41 -0
  56. package/runtime/install-local-cpu-runtime.sh +23 -0
  57. package/runtime/install-scientific-runtime.sh +153 -0
  58. package/runtime/mineru/parse.py +62 -0
  59. package/runtime/mineru/requirements.txt +4 -0
  60. package/runtime/requirements-baseline.txt +38 -0
  61. package/schemas/cap/v2/activity.schema.json +47 -0
  62. package/schemas/cap/v2/agent.schema.json +32 -0
  63. package/schemas/cap/v2/assertion.schema.json +110 -0
  64. package/schemas/cap/v2/descriptor.schema.json +243 -0
  65. package/schemas/cap/v2/entity.schema.json +64 -0
  66. package/schemas/cap/v2/manifest.schema.json +67 -0
  67. package/schemas/cap/v2/relation.schema.json +82 -0
  68. package/schemas/compute-catalog.schema.json +63 -0
  69. package/schemas/compute-decision.schema.json +27 -0
  70. package/schemas/execution-contract.schema.json +1024 -0
  71. package/schemas/research-card.schema.json +30 -0
  72. package/schemas/research-inventory-draft.schema.json +366 -0
  73. package/schemas/research.schema.json +1044 -0
  74. package/schemas/result.schema.json +173 -0
  75. package/schemas/verification-policy.schema.json +47 -0
  76. package/schemas/verified-conclusion.schema.json +58 -0
  77. package/schemas/workspace-summary.schema.json +24 -0
  78. package/scripts/build-arkgraph-view.mjs +12 -0
  79. package/scripts/check-execution-feasibility.mjs +24 -0
  80. package/scripts/check-syntax.mjs +15 -0
  81. package/scripts/deterministic-asset-preparation.py +438 -0
  82. package/scripts/package-cap.mjs +23 -0
  83. package/scripts/package-local-agent.mjs +23 -0
  84. package/scripts/preview-arkgraph.mjs +25 -0
  85. package/scripts/replay-research-compiler-candidate.mjs +134 -0
  86. package/scripts/review-compiler-sources.mjs +44 -0
  87. package/scripts/run-asset-preparation.sh +17 -0
  88. package/scripts/run-research-plan.mjs +98 -0
  89. package/scripts/validate-asset-preparation.py +290 -0
  90. package/scripts/verify-local-runtime.mjs +57 -0
  91. package/scripts/verify-npm-package.mjs +57 -0
  92. package/src/adapters/paper2agent.mjs +107 -0
  93. package/src/assets/cache.mjs +159 -0
  94. package/src/assets/compute.mjs +98 -0
  95. package/src/assets/executor.mjs +145 -0
  96. package/src/assets/lifecycle.mjs +213 -0
  97. package/src/assets/manifest.mjs +242 -0
  98. package/src/assets/opportunistic-preparation.mjs +81 -0
  99. package/src/assets/plan.mjs +411 -0
  100. package/src/assets/prompts.mjs +29 -0
  101. package/src/assets/public-asset-probe.mjs +525 -0
  102. package/src/assets/qualification.mjs +119 -0
  103. package/src/assets/readiness.mjs +130 -0
  104. package/src/assets/reproduction-admission.mjs +355 -0
  105. package/src/assets/requirements.mjs +152 -0
  106. package/src/assets/source-grounding.mjs +341 -0
  107. package/src/assets/source-policy.mjs +118 -0
  108. package/src/autodl/client.mjs +260 -0
  109. package/src/autodl/ssh.mjs +380 -0
  110. package/src/autodl/tools.mjs +129 -0
  111. package/src/cap/redaction.mjs +38 -0
  112. package/src/cap/v2/archive.mjs +152 -0
  113. package/src/cap/v2/attestation.mjs +204 -0
  114. package/src/cap/v2/canonical-json.mjs +114 -0
  115. package/src/cap/v2/compilation-artifact.mjs +240 -0
  116. package/src/cap/v2/core.mjs +282 -0
  117. package/src/cap/v2/measurement-assessment-records.mjs +23 -0
  118. package/src/cap/v2/pipeline-artifact.mjs +922 -0
  119. package/src/cap/v2/read.mjs +41 -0
  120. package/src/cap/v2/reassessment-artifact.mjs +383 -0
  121. package/src/cap/v2/research-artifact.mjs +231 -0
  122. package/src/cap/v2/research-map-records.mjs +46 -0
  123. package/src/cap/v2/research-object-records.mjs +163 -0
  124. package/src/cap/v2/research-records.mjs +187 -0
  125. package/src/cap/v2/verify.mjs +642 -0
  126. package/src/cli.mjs +1146 -0
  127. package/src/compute/autodl-pro-compiler.mjs +347 -0
  128. package/src/compute/autodl-pro-executor.mjs +459 -0
  129. package/src/compute/autodl-pro-job.mjs +843 -0
  130. package/src/compute/autodl-pro-network.mjs +295 -0
  131. package/src/compute/autodl-pro-remote.mjs +810 -0
  132. package/src/compute/autodl-pro-staging.mjs +117 -0
  133. package/src/compute/campaign.mjs +110 -0
  134. package/src/compute/catalog.mjs +123 -0
  135. package/src/compute/checkpoint-protocol.mjs +154 -0
  136. package/src/compute/codex-account-lock.mjs +111 -0
  137. package/src/compute/codex-account-session.mjs +107 -0
  138. package/src/compute/compiler-profile.mjs +38 -0
  139. package/src/compute/compiler-router.mjs +23 -0
  140. package/src/compute/coordinator-recovery.mjs +210 -0
  141. package/src/compute/executor-router.mjs +29 -0
  142. package/src/compute/gcp-batch-compiler.mjs +685 -0
  143. package/src/compute/gcp-batch-executor.mjs +1215 -0
  144. package/src/compute/gcp-batch-failure.mjs +92 -0
  145. package/src/compute/gcp-batch-job.mjs +527 -0
  146. package/src/compute/gcp-batch-lifecycle.mjs +81 -0
  147. package/src/compute/gcp-checkpoint-worker.mjs +1633 -0
  148. package/src/compute/local-codex-compiler.mjs +52 -0
  149. package/src/compute/measurement-hardware.mjs +128 -0
  150. package/src/compute/remote-attempt.mjs +226 -0
  151. package/src/compute/requirements.mjs +124 -0
  152. package/src/compute/research-phases.mjs +48 -0
  153. package/src/compute/scheduler.mjs +452 -0
  154. package/src/compute/shared-workloads.mjs +26 -0
  155. package/src/compute/stage-archive.mjs +79 -0
  156. package/src/contracts/campaign-contract.mjs +52 -0
  157. package/src/contracts/execution-contract.mjs +819 -0
  158. package/src/contracts/execution-mode.mjs +19 -0
  159. package/src/contracts/execution-timeouts.mjs +45 -0
  160. package/src/contracts/execution-workload.mjs +68 -0
  161. package/src/contracts/preflight-schema.mjs +25 -0
  162. package/src/contracts/public-contract.mjs +63 -0
  163. package/src/contracts/subject-tags.mjs +31 -0
  164. package/src/dashboard/data.mjs +898 -0
  165. package/src/dashboard/server.mjs +79 -0
  166. package/src/dashboard/static/dashboard.css +366 -0
  167. package/src/dashboard/static/dashboard.js +560 -0
  168. package/src/dashboard/static/index.html +85 -0
  169. package/src/deployment/community-policy.mjs +9 -0
  170. package/src/deployment/environment.mjs +112 -0
  171. package/src/deployment/guided.mjs +98 -0
  172. package/src/deployment/handoff.mjs +102 -0
  173. package/src/deployment/local-contract.mjs +31 -0
  174. package/src/deployment/local.mjs +100 -0
  175. package/src/deployment/prepare.mjs +46 -0
  176. package/src/deployment/recipe.mjs +108 -0
  177. package/src/deployment/supplement.mjs +51 -0
  178. package/src/deployment/terminal.mjs +43 -0
  179. package/src/diagnosis/renderer.mjs +75 -0
  180. package/src/diagnosis/target-failure.mjs +46 -0
  181. package/src/evidence/parser-registry.mjs +54 -0
  182. package/src/evidence/parsers/fasttext-classification.mjs +82 -0
  183. package/src/evidence/parsers/json-scalar.mjs +96 -0
  184. package/src/evidence/parsers/simcse-senteval.mjs +104 -0
  185. package/src/evidence/parsers/starspace-classification.mjs +78 -0
  186. package/src/evidence/registry.mjs +147 -0
  187. package/src/execution/runner-audit.mjs +473 -0
  188. package/src/gcp/auth.mjs +106 -0
  189. package/src/gcp/batch-client.mjs +120 -0
  190. package/src/gcp/resource-discovery.mjs +177 -0
  191. package/src/gcp/rest.mjs +82 -0
  192. package/src/gcp/secret-manager.mjs +34 -0
  193. package/src/gcp/signed-url.mjs +133 -0
  194. package/src/gcp/storage.mjs +220 -0
  195. package/src/graph/command.mjs +41 -0
  196. package/src/graph/execution.mjs +97 -0
  197. package/src/graph/model.mjs +37 -0
  198. package/src/graph/presentation.mjs +110 -0
  199. package/src/graph/query.mjs +159 -0
  200. package/src/graph/research-relations.mjs +69 -0
  201. package/src/graph/source-page.mjs +12 -0
  202. package/src/graph/source-preview.mjs +34 -0
  203. package/src/graph/validate.mjs +76 -0
  204. package/src/job.mjs +496 -0
  205. package/src/network/autodl-routing-proxy.mjs +462 -0
  206. package/src/network/egress-proxy.mjs +158 -0
  207. package/src/observability/event-contract.mjs +230 -0
  208. package/src/observability/pipeline-monitor.mjs +166 -0
  209. package/src/pipeline/orchestrator.mjs +1281 -0
  210. package/src/pipeline/recovery-error.mjs +11 -0
  211. package/src/pipeline/replay.mjs +304 -0
  212. package/src/pipeline/shared-execution.mjs +115 -0
  213. package/src/pipeline/stage-checkpoint.mjs +86 -0
  214. package/src/pipeline/stage-recovery.mjs +101 -0
  215. package/src/pipeline/targets.mjs +110 -0
  216. package/src/process.mjs +143 -0
  217. package/src/protocol.mjs +312 -0
  218. package/src/provider/codex-account.mjs +44 -0
  219. package/src/provider/codex-completion.mjs +49 -0
  220. package/src/provider/completion.mjs +292 -0
  221. package/src/provider/model-client.mjs +44 -0
  222. package/src/provider/model-route.mjs +29 -0
  223. package/src/provider/openrouter-readiness.mjs +189 -0
  224. package/src/provider/reader-bridge.mjs +35 -0
  225. package/src/provider/relay.mjs +263 -0
  226. package/src/provider/runtime-auth.mjs +40 -0
  227. package/src/public/cap.d.mts +90 -0
  228. package/src/public/cap.mjs +12 -0
  229. package/src/public/contracts.d.mts +2 -0
  230. package/src/public/host.mjs +171 -0
  231. package/src/public/operations.d.mts +11 -0
  232. package/src/public/presentation.d.mts +4 -0
  233. package/src/records/views.mjs +26 -0
  234. package/src/remote/command.mjs +178 -0
  235. package/src/remote/ssh.mjs +59 -0
  236. package/src/repository-origin.mjs +81 -0
  237. package/src/reproduction/evidence-feedback.mjs +96 -0
  238. package/src/reproduction/incomplete-initialization.mjs +25 -0
  239. package/src/reproduction/lifecycle.mjs +253 -0
  240. package/src/reproduction/plan.mjs +132 -0
  241. package/src/reproduction/prompts.mjs +70 -0
  242. package/src/reproduction/runner.mjs +188 -0
  243. package/src/reproduction/summary.mjs +130 -0
  244. package/src/reproduction/workspace-mode.mjs +7 -0
  245. package/src/research/automatic-admission.mjs +156 -0
  246. package/src/research/compiler-coverage.mjs +85 -0
  247. package/src/research/compiler-failure.mjs +24 -0
  248. package/src/research/compiler-normalization-guards.mjs +112 -0
  249. package/src/research/compiler-repair.mjs +3 -0
  250. package/src/research/compiler.mjs +853 -0
  251. package/src/research/continuation-selection.mjs +26 -0
  252. package/src/research/execution-graph-context.mjs +43 -0
  253. package/src/research/experiment-importance.mjs +15 -0
  254. package/src/research/inventory-handoff.mjs +104 -0
  255. package/src/research/inventory-revisions.mjs +32 -0
  256. package/src/research/mineru-local.mjs +73 -0
  257. package/src/research/paper-command.mjs +19 -0
  258. package/src/research/paper-markdown.mjs +180 -0
  259. package/src/research/paper-source-map.mjs +69 -0
  260. package/src/research/planning-policy.mjs +88 -0
  261. package/src/research/reference-materials.mjs +11 -0
  262. package/src/research/reproduction-scope.mjs +30 -0
  263. package/src/research/research-map.mjs +94 -0
  264. package/src/research/research-objects.mjs +88 -0
  265. package/src/research/source-discovery.mjs +646 -0
  266. package/src/research/source-observations.mjs +75 -0
  267. package/src/research/source-review-cli-mcp.mjs +26 -0
  268. package/src/research/source-review-input.mjs +209 -0
  269. package/src/research/source-review-local-codex.mjs +36 -0
  270. package/src/research/source-review-model.mjs +70 -0
  271. package/src/research/source-review.mjs +173 -0
  272. package/src/research/structure.mjs +3163 -0
  273. package/src/research-card/renderer.mjs +277 -0
  274. package/src/research-card/verified-conclusion.mjs +143 -0
  275. package/src/results/output-registry.mjs +183 -0
  276. package/src/runtime/claude-code.mjs +52 -0
  277. package/src/runtime/codex-capacity-retry.mjs +87 -0
  278. package/src/runtime/codex.mjs +64 -0
  279. package/src/runtime/config.mjs +157 -0
  280. package/src/runtime/final-output.mjs +40 -0
  281. package/src/runtime/index.mjs +21 -0
  282. package/src/runtime/local-codex.mjs +74 -0
  283. package/src/runtime/opencode.mjs +95 -0
  284. package/src/runtime/prompt.mjs +13 -0
  285. package/src/sandbox/docker.mjs +363 -0
  286. package/src/settings/command.mjs +297 -0
  287. package/src/settings/store.mjs +119 -0
  288. package/src/telemetry/pricing.mjs +68 -0
  289. package/src/telemetry/usage.mjs +265 -0
  290. package/src/terminal/events.mjs +97 -0
  291. package/src/terminal/input.mjs +40 -0
  292. package/src/terminal/plain.mjs +40 -0
  293. package/src/terminal/remote-stream.mjs +22 -0
  294. package/src/terminal/screen.mjs +214 -0
  295. package/src/terminal/transcript.mjs +69 -0
  296. package/src/util.mjs +107 -0
  297. package/src/verification/ai-assessor.mjs +534 -0
  298. package/src/verification/claim-evaluator.mjs +242 -0
  299. package/src/verification/evidence-context.mjs +165 -0
  300. package/src/verification/evidence-reader.mjs +95 -0
  301. package/src/verification/integrity.mjs +570 -0
  302. package/src/verification/tolerance.mjs +32 -0
  303. package/src/workloads/cpu-research-preparation.mjs +56 -0
  304. package/src/workloads/definition.mjs +74 -0
  305. package/src/workloads/phase-aware-reproduction.mjs +46 -0
  306. package/src/workloads/reproduction.mjs +85 -0
  307. package/src/workspace/command.mjs +242 -0
  308. package/src/workspace/control.mjs +49 -0
  309. package/src/workspace/entry.mjs +28 -0
  310. package/src/workspace/input.mjs +93 -0
  311. package/src/workspace/interactive.mjs +94 -0
  312. package/src/workspace/jobs.mjs +418 -0
  313. package/src/workspace/session.mjs +97 -0
  314. package/src/workspace/worker.mjs +137 -0
  315. package/ui/arkgraph/ambient-motion.mjs +10 -0
  316. package/ui/arkgraph/app.jsx +153 -0
  317. package/ui/arkgraph/boot.js +6 -0
  318. package/ui/arkgraph/camera-motion.mjs +20 -0
  319. package/ui/arkgraph/context-reveal.mjs +39 -0
  320. package/ui/arkgraph/details.css +3 -0
  321. package/ui/arkgraph/entry.jsx +28 -0
  322. package/ui/arkgraph/experiment-curves.mjs +17 -0
  323. package/ui/arkgraph/experiment-selection.mjs +15 -0
  324. package/ui/arkgraph/experiment-style.css +26 -0
  325. package/ui/arkgraph/experiment-ui.jsx +32 -0
  326. package/ui/arkgraph/frame.html +1 -0
  327. package/ui/arkgraph/graph-gestures.mjs +62 -0
  328. package/ui/arkgraph/label-layout.mjs +57 -0
  329. package/ui/arkgraph/locales/en.json +229 -0
  330. package/ui/arkgraph/locales/source-types.json +15 -0
  331. package/ui/arkgraph/localization-build.mjs +27 -0
  332. package/ui/arkgraph/material-build.mjs +23 -0
  333. package/ui/arkgraph/material-colors.mjs +39 -0
  334. package/ui/arkgraph/material-style.css +15 -0
  335. package/ui/arkgraph/open-graph.jsx +326 -0
  336. package/ui/arkgraph/outline.jsx +49 -0
  337. package/ui/arkgraph/package-lock.json +888 -0
  338. package/ui/arkgraph/package.json +17 -0
  339. package/ui/arkgraph/reading-layout.mjs +130 -0
  340. package/ui/arkgraph/reading-presentation.mjs +73 -0
  341. package/ui/arkgraph/record-detail.css +51 -0
  342. package/ui/arkgraph/record-details.jsx +29 -0
  343. package/ui/arkgraph/research-types.mjs +31 -0
  344. package/ui/arkgraph/selection-mark.jsx +6 -0
  345. package/ui/arkgraph/soft-spine.mjs +26 -0
  346. package/ui/arkgraph/steering-style.css +187 -0
  347. 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
+ }