@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,119 @@
1
+ import path from "node:path";
2
+ import { homedir } from "node:os";
3
+ import { mkdir, readFile, writeFile, rename } from "node:fs/promises";
4
+ import { resolveAgentRuntime } from "../runtime/config.mjs";
5
+
6
+ export const settingsPath = (env = process.env) =>
7
+ path.join(
8
+ env.CITEARK_CONFIG_DIR ?? path.join(homedir(), ".citeark"),
9
+ "config.json",
10
+ );
11
+ export async function readSettings(file = settingsPath()) {
12
+ try {
13
+ return JSON.parse(await readFile(file, "utf8"));
14
+ } catch (error) {
15
+ if (error.code !== "ENOENT") throw error;
16
+ return { schemaVersion: 1, models: {}, compute: {} };
17
+ }
18
+ }
19
+ export async function writeSettings(settings, file = settingsPath()) {
20
+ await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
21
+ const temporary = `${file}.${process.pid}.tmp`;
22
+ await writeFile(temporary, JSON.stringify(settings, null, 2) + "\n", {
23
+ mode: 0o600,
24
+ });
25
+ await rename(temporary, file);
26
+ }
27
+ export function profileName(name) {
28
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(name ?? ""))
29
+ throw Error(
30
+ "Use a profile name with letters, numbers, dashes or underscores.",
31
+ );
32
+ return name;
33
+ }
34
+ export function modelProfile(value) {
35
+ const fields = [
36
+ "runtime",
37
+ "apiBaseUrl",
38
+ "model",
39
+ "apiKeyEnv",
40
+ "effort",
41
+ "subagentModel",
42
+ "maxBudgetUsd",
43
+ ];
44
+ const result = Object.fromEntries(
45
+ fields.filter((k) => value[k] !== undefined).map((k) => [k, value[k]]),
46
+ );
47
+ if (result.apiKeyEnv && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(result.apiKeyEnv))
48
+ throw Error("API key environment variable name is invalid.");
49
+ resolveAgentRuntime({ ...result, requireApi: false });
50
+ return result;
51
+ }
52
+ export function resolveModelProfile(settings, name, env = process.env) {
53
+ const selected = name ?? settings.defaultModel;
54
+ if (!selected) return null;
55
+ const profile = settings.models[selected];
56
+ if (!profile)
57
+ throw Error(`Unknown model profile: ${selected}. Run citeark model list.`);
58
+ return {
59
+ ...modelProfile(profile),
60
+ apiKey: profile.apiKeyEnv ? env[profile.apiKeyEnv] : undefined,
61
+ apiKeySource: profile.apiKeyEnv
62
+ ? `env:${profile.apiKeyEnv}`
63
+ : "local-session",
64
+ };
65
+ }
66
+ export function savedModel(runtime) {
67
+ return modelProfile({
68
+ ...runtime,
69
+ apiKeyEnv:
70
+ runtime.apiKeyEnv ??
71
+ (runtime.apiKeySource?.startsWith("env:")
72
+ ? runtime.apiKeySource.slice(4)
73
+ : undefined),
74
+ });
75
+ }
76
+ export function computeProfile(value) {
77
+ if (!["local", "ssh", "catalog"].includes(value.type))
78
+ throw Error("Compute type must be local, ssh or catalog.");
79
+ if (value.type === "ssh") {
80
+ if (!value.host || value.host.startsWith("-") || /[\s\0]/.test(value.host))
81
+ throw Error("Enter an SSH host or SSH config alias.");
82
+ if (!value.directory?.startsWith("/"))
83
+ throw Error("The remote workspace root must be an absolute path.");
84
+ if (
85
+ value.port !== undefined &&
86
+ (!Number.isInteger(value.port) || value.port < 1 || value.port > 65535)
87
+ )
88
+ throw Error("Invalid SSH port.");
89
+ return {
90
+ type: "ssh",
91
+ host: value.host,
92
+ directory: value.directory,
93
+ ...(value.port ? { port: value.port } : {}),
94
+ ...(value.identityFile
95
+ ? { identityFile: path.resolve(value.identityFile) }
96
+ : {}),
97
+ ...(value.configFile
98
+ ? { configFile: path.resolve(value.configFile) }
99
+ : {}),
100
+ command: value.command ?? "citeark",
101
+ };
102
+ }
103
+ if (value.type === "catalog") {
104
+ if (!value.catalog) throw Error("Provide a compute catalog file.");
105
+ return {
106
+ type: "catalog",
107
+ catalog: path.resolve(value.catalog),
108
+ ...(value.policy ? { policy: path.resolve(value.policy) } : {}),
109
+ ...(value.profile ? { profile: value.profile } : {}),
110
+ };
111
+ }
112
+ if (value.device && !["cpu", "cuda"].includes(value.device))
113
+ throw Error("Local device must be cpu or cuda.");
114
+ return {
115
+ type: "local",
116
+ device: value.device ?? "cpu",
117
+ ...(value.image ? { image: value.image } : {}),
118
+ };
119
+ }
@@ -0,0 +1,68 @@
1
+ const RATES_CNY_PER_MILLION = Object.freeze({
2
+ flash: {
3
+ idle: { cacheHitInput: 0.05, cacheMissInput: 1.5, output: 4.5 },
4
+ peak: { cacheHitInput: 0.1, cacheMissInput: 3, output: 9 },
5
+ },
6
+ pro: {
7
+ idle: { cacheHitInput: 0.15, cacheMissInput: 4.5, output: 13.5 },
8
+ peak: { cacheHitInput: 0.3, cacheMissInput: 9, output: 27 },
9
+ },
10
+ });
11
+
12
+ export function estimateDeepSeekCost(usage, { defaultModel = "deepseek-v4-pro" } = {}) {
13
+ const modelEntries = Object.entries(usage?.models ?? {});
14
+ const entries = modelEntries.length
15
+ ? modelEntries
16
+ : [[defaultModel, {
17
+ inputTokens: usage?.inputTokens,
18
+ outputTokens: usage?.outputTokens,
19
+ cacheReadInputTokens: usage?.cacheReadInputTokens,
20
+ cacheCreationInputTokens: usage?.cacheCreationInputTokens,
21
+ }]];
22
+ const byModel = {};
23
+ let idleCny = 0;
24
+ let peakCny = 0;
25
+
26
+ for (const [model, modelUsage] of entries) {
27
+ const tier = model.toLowerCase().includes("flash") ? "flash" : "pro";
28
+ const cacheHitTokens = number(modelUsage.cacheReadInputTokens);
29
+ const cacheMissTokens = number(modelUsage.inputTokens) + number(modelUsage.cacheCreationInputTokens);
30
+ const outputTokens = number(modelUsage.outputTokens);
31
+ const idle = costFor(RATES_CNY_PER_MILLION[tier].idle, cacheHitTokens, cacheMissTokens, outputTokens);
32
+ const peak = costFor(RATES_CNY_PER_MILLION[tier].peak, cacheHitTokens, cacheMissTokens, outputTokens);
33
+ idleCny += idle;
34
+ peakCny += peak;
35
+ byModel[model] = {
36
+ tier,
37
+ cacheHitTokens,
38
+ cacheMissTokens,
39
+ outputTokens,
40
+ idleCny: rounded(idle),
41
+ peakCny: rounded(peak),
42
+ };
43
+ }
44
+
45
+ return {
46
+ currency: "CNY",
47
+ source: "deepseek-v4-pricing-table-2026-08",
48
+ idleCny: rounded(idleCny),
49
+ peakCny: rounded(peakCny),
50
+ byModel,
51
+ };
52
+ }
53
+
54
+ function costFor(rates, cacheHitTokens, cacheMissTokens, outputTokens) {
55
+ return (
56
+ cacheHitTokens * rates.cacheHitInput +
57
+ cacheMissTokens * rates.cacheMissInput +
58
+ outputTokens * rates.output
59
+ ) / 1_000_000;
60
+ }
61
+
62
+ function number(value) {
63
+ return Number.isFinite(Number(value)) ? Number(value) : 0;
64
+ }
65
+
66
+ function rounded(value) {
67
+ return Math.round(value * 1_000_000) / 1_000_000;
68
+ }
@@ -0,0 +1,265 @@
1
+ import { readFile, readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ const EMPTY_USAGE = Object.freeze({
5
+ inputTokens: 0,
6
+ outputTokens: 0,
7
+ cacheReadInputTokens: 0,
8
+ cacheCreationInputTokens: 0,
9
+ totalTokens: 0,
10
+ costUsd: 0,
11
+ costStatus: "unavailable",
12
+ turns: 0,
13
+ models: {},
14
+ });
15
+
16
+ export async function readAgentUsage(tracePath, { runtimeHome, model } = {}) {
17
+ const traceUsage = await readTraceUsage(tracePath);
18
+ const codexUsage = runtimeHome
19
+ ? await readCodexRolloutUsage(path.join(runtimeHome, "codex", "sessions"), { model })
20
+ : null;
21
+ if (!codexUsage || codexUsage.totalTokens === 0) return traceUsage;
22
+ return {
23
+ ...codexUsage,
24
+ costUsd: Math.max(codexUsage.costUsd, traceUsage.costUsd),
25
+ costStatus: traceUsage.costStatus,
26
+ };
27
+ }
28
+
29
+ async function readTraceUsage(tracePath) {
30
+ let raw;
31
+ try {
32
+ raw = await readFile(tracePath, "utf8");
33
+ } catch {
34
+ return structuredClone(EMPTY_USAGE);
35
+ }
36
+
37
+ const usageEvents = [];
38
+ for (const line of raw.split("\n")) {
39
+ if (!line.trim()) continue;
40
+ try {
41
+ const event = JSON.parse(line);
42
+ const normalized = normalizeUsageEvent(event);
43
+ if (normalized) usageEvents.push(normalized);
44
+ } catch {
45
+ // Traces can end with a partial line if a process is interrupted.
46
+ }
47
+ }
48
+ return usageEvents.reduce(
49
+ (total, usage) => mergeAgentUsage(total, usage),
50
+ structuredClone(EMPTY_USAGE),
51
+ );
52
+ }
53
+
54
+ export async function readCodexRolloutUsage(sessionsDirectory, { model: fallbackModel } = {}) {
55
+ const files = await jsonlFiles(sessionsDirectory);
56
+ const sessions = [];
57
+ for (const file of files) {
58
+ let raw;
59
+ try {
60
+ raw = await readFile(file, "utf8");
61
+ } catch {
62
+ continue;
63
+ }
64
+ let latest = null;
65
+ let model = fallbackModel ?? null;
66
+ let turns = 0;
67
+ for (const line of raw.split("\n")) {
68
+ if (!line.trim()) continue;
69
+ try {
70
+ const event = JSON.parse(line);
71
+ if (event.type === "turn_context") {
72
+ turns += 1;
73
+ model = event.payload?.model ?? model;
74
+ }
75
+ if (
76
+ event.type === "event_msg"
77
+ && event.payload?.type === "token_count"
78
+ && event.payload?.info?.total_token_usage
79
+ ) {
80
+ latest = event.payload.info.total_token_usage;
81
+ }
82
+ } catch {
83
+ // A force-stopped Codex rollout can end with a partial JSONL line.
84
+ }
85
+ }
86
+ if (!latest) continue;
87
+ const inputWithCache = number(latest.input_tokens);
88
+ const cacheReadInputTokens = Math.min(inputWithCache, number(latest.cached_input_tokens));
89
+ const outputTokens = number(latest.output_tokens);
90
+ const inputTokens = Math.max(0, inputWithCache - cacheReadInputTokens);
91
+ const modelName = model ?? "codex-unknown-model";
92
+ sessions.push({
93
+ inputTokens,
94
+ outputTokens,
95
+ cacheReadInputTokens,
96
+ cacheCreationInputTokens: 0,
97
+ costUsd: 0,
98
+ costStatus: "unavailable",
99
+ turns: Math.max(1, turns),
100
+ models: {
101
+ [modelName]: {
102
+ inputTokens,
103
+ outputTokens,
104
+ cacheReadInputTokens,
105
+ cacheCreationInputTokens: 0,
106
+ costUsd: 0,
107
+ },
108
+ },
109
+ });
110
+ }
111
+ return mergeAgentUsage(...sessions);
112
+ }
113
+
114
+ async function jsonlFiles(root) {
115
+ let entries;
116
+ try {
117
+ entries = await readdir(root, { withFileTypes: true });
118
+ } catch {
119
+ return [];
120
+ }
121
+ const files = [];
122
+ for (const entry of entries) {
123
+ const target = path.join(root, entry.name);
124
+ if (entry.isDirectory()) files.push(...await jsonlFiles(target));
125
+ else if (entry.isFile() && entry.name.endsWith(".jsonl")) files.push(target);
126
+ }
127
+ return files.sort();
128
+ }
129
+
130
+ export function mergeAgentUsage(...values) {
131
+ const merged = structuredClone(EMPTY_USAGE);
132
+ let pricedSources = 0;
133
+ let unpricedSources = 0;
134
+ let subscriptionSources = 0;
135
+ for (const value of values) {
136
+ if (!value) continue;
137
+ if (hasUsage(value)) {
138
+ if (value.costStatus === "subscription" && number(value.costUsd) === 0) subscriptionSources += 1;
139
+ else if (value.costStatus === "reported" || number(value.costUsd) > 0) pricedSources += 1;
140
+ else if (value.costStatus === "partial") {
141
+ pricedSources += 1;
142
+ unpricedSources += 1;
143
+ } else unpricedSources += 1;
144
+ }
145
+ merged.inputTokens += number(value.inputTokens);
146
+ merged.outputTokens += number(value.outputTokens);
147
+ merged.cacheReadInputTokens += number(value.cacheReadInputTokens);
148
+ merged.cacheCreationInputTokens += number(value.cacheCreationInputTokens);
149
+ merged.costUsd += number(value.costUsd);
150
+ merged.turns += number(value.turns);
151
+ for (const [model, usage] of Object.entries(value.models ?? {})) {
152
+ const previous = merged.models[model] ?? {
153
+ inputTokens: 0,
154
+ outputTokens: 0,
155
+ cacheReadInputTokens: 0,
156
+ cacheCreationInputTokens: 0,
157
+ costUsd: 0,
158
+ };
159
+ merged.models[model] = {
160
+ inputTokens: previous.inputTokens + number(usage.inputTokens),
161
+ outputTokens: previous.outputTokens + number(usage.outputTokens),
162
+ cacheReadInputTokens:
163
+ previous.cacheReadInputTokens + number(usage.cacheReadInputTokens),
164
+ cacheCreationInputTokens:
165
+ previous.cacheCreationInputTokens + number(usage.cacheCreationInputTokens),
166
+ costUsd: previous.costUsd + number(usage.costUsd),
167
+ };
168
+ }
169
+ }
170
+ merged.totalTokens =
171
+ merged.inputTokens +
172
+ merged.outputTokens +
173
+ merged.cacheReadInputTokens +
174
+ merged.cacheCreationInputTokens;
175
+ merged.costUsd = roundedCost(merged.costUsd);
176
+ if (subscriptionSources > 0) merged.billingMode = pricedSources === 0 && unpricedSources === 0 ? "subscription" : "mixed";
177
+ merged.costStatus = subscriptionSources > 0 && pricedSources === 0 && unpricedSources === 0 ? "subscription" : pricedSources === 0
178
+ ? "unavailable"
179
+ : unpricedSources === 0 ? "reported" : "partial";
180
+ return merged;
181
+ }
182
+
183
+ function usageFromResult(event) {
184
+ const usage = event.usage ?? {};
185
+ const models = {};
186
+ for (const [model, value] of Object.entries(event.modelUsage ?? {})) {
187
+ models[model] = {
188
+ inputTokens: number(value.inputTokens),
189
+ outputTokens: number(value.outputTokens),
190
+ cacheReadInputTokens: number(value.cacheReadInputTokens),
191
+ cacheCreationInputTokens: number(value.cacheCreationInputTokens),
192
+ costUsd: number(value.costUSD),
193
+ };
194
+ }
195
+ return {
196
+ inputTokens: number(usage.input_tokens),
197
+ outputTokens: number(usage.output_tokens),
198
+ cacheReadInputTokens: number(usage.cache_read_input_tokens),
199
+ cacheCreationInputTokens: number(usage.cache_creation_input_tokens),
200
+ costUsd: number(event.total_cost_usd),
201
+ costStatus: finiteOwn(event, "total_cost_usd") ? "reported" : "unavailable",
202
+ turns: number(event.num_turns),
203
+ models,
204
+ };
205
+ }
206
+
207
+ function normalizeUsageEvent(event) {
208
+ if (event.type === "result" && event.usage) return usageFromResult(event);
209
+
210
+ if (event.type === "turn.completed" && event.usage) {
211
+ const cached = number(event.usage.cached_input_tokens);
212
+ return {
213
+ inputTokens: Math.max(0, number(event.usage.input_tokens) - cached),
214
+ outputTokens: number(event.usage.output_tokens),
215
+ cacheReadInputTokens: cached,
216
+ cacheCreationInputTokens: 0,
217
+ costUsd: 0,
218
+ costStatus: "unavailable",
219
+ turns: 1,
220
+ models: {},
221
+ };
222
+ }
223
+
224
+ const part = event.part ?? event.properties?.part;
225
+ if (
226
+ (event.type === "step_finish" || event.type === "step-finish" || part?.type === "step-finish") &&
227
+ part?.tokens
228
+ ) {
229
+ const cached = number(part.tokens.cache?.read);
230
+ return {
231
+ inputTokens: number(part.tokens.input),
232
+ outputTokens: number(part.tokens.output) + number(part.tokens.reasoning),
233
+ cacheReadInputTokens: cached,
234
+ cacheCreationInputTokens: number(part.tokens.cache?.write),
235
+ costUsd: number(part.cost),
236
+ costStatus: finiteOwn(part, "cost") ? "reported" : "unavailable",
237
+ turns: 1,
238
+ models: {},
239
+ };
240
+ }
241
+ return null;
242
+ }
243
+
244
+ function number(value) {
245
+ return Number.isFinite(Number(value)) ? Number(value) : 0;
246
+ }
247
+
248
+ function finiteOwn(value, key) {
249
+ return value && Object.prototype.hasOwnProperty.call(value, key)
250
+ && typeof value[key] === "number" && Number.isFinite(value[key]);
251
+ }
252
+
253
+ function hasUsage(value) {
254
+ return number(value?.totalTokens) > 0
255
+ || number(value?.inputTokens) > 0
256
+ || number(value?.outputTokens) > 0
257
+ || number(value?.cacheReadInputTokens) > 0
258
+ || number(value?.cacheCreationInputTokens) > 0
259
+ || number(value?.turns) > 0
260
+ || Object.keys(value?.models ?? {}).length > 0;
261
+ }
262
+
263
+ function roundedCost(value) {
264
+ return Math.round(value * 1_000_000) / 1_000_000;
265
+ }
@@ -0,0 +1,97 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { StringDecoder } from 'node:string_decoder';
3
+
4
+ // The CLI subscribes locally. Hosted consumers keep their own output policy.
5
+ const session = new AsyncLocalStorage();
6
+ export const withSessionEvents = (sink, action) => session.run(sink, action);
7
+ export const hasSessionEvents = () => Boolean(session.getStore());
8
+ export function sessionEvent(event) { session.getStore()?.({ at: new Date().toISOString(), ...event }); }
9
+
10
+ export function createRuntimeStream({ source, runtime, emit = sessionEvent }) {
11
+ const decoder = new StringDecoder('utf8');
12
+ let pending = '';
13
+ let sequence = 0;
14
+ const claudeMessages = new Map();
15
+ const texts = new Map();
16
+ const send = (kind, text, id, extra = {}) => {
17
+ if (typeof text !== 'string' || !text) return;
18
+ const key = `${source}:${id ?? ++sequence}`;
19
+ if (texts.get(key) === text) return;
20
+ texts.set(key, text);
21
+ emit({ kind, text, id: key, source, runtime, ...extra });
22
+ };
23
+ const content = (blocks, prefix) => {
24
+ if (typeof blocks === 'string') { send('tool-output', blocks, prefix); return; }
25
+ for (const [index, block] of (blocks ?? []).entries()) {
26
+ const id = `${prefix}:${block.id ?? block.tool_use_id ?? index}`;
27
+ if (block.type === 'text') send('assistant', block.text, id);
28
+ if (block.type === 'tool_use') send('tool', `${block.name}\n${JSON.stringify(block.input, null, 2)}`, id);
29
+ if (block.type === 'tool_result') {
30
+ const text = typeof block.content === 'string' ? block.content
31
+ : (block.content ?? []).filter(item => item.type === 'text').map(item => item.text).join('\n');
32
+ send('tool-output', text, id, { failed: block.is_error === true });
33
+ }
34
+ }
35
+ };
36
+ const record = value => {
37
+ if (value.type?.startsWith('citeark.')) return;
38
+ if (value.type === 'assistant' || value.type === 'user') {
39
+ content(value.message?.content, value.message?.id ?? value.uuid ?? `message-${++sequence}`);
40
+ } else if (value.type === 'stream_event') {
41
+ const event = value.event;
42
+ const parent = value.parent_tool_use_id ?? 'main';
43
+ if (event?.type === 'message_start') claudeMessages.set(parent, event.message.id);
44
+ if (event?.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
45
+ const id = `${claudeMessages.get(parent) ?? parent}:${event.index}`;
46
+ send('assistant', (texts.get(`${source}:${id}`) ?? '') + event.delta.text, id);
47
+ }
48
+ } else if (value.type?.startsWith('item.') && value.item) {
49
+ const item = value.item;
50
+ const id = item.id ?? `item-${++sequence}`;
51
+ if (item.type === 'agent_message') send('assistant', item.text, id);
52
+ if (item.type === 'command_execution') {
53
+ send('tool', `$ ${item.command}`, `${id}:command`);
54
+ send('tool-output', item.aggregated_output, `${id}:output`, { status: item.status, exitCode: item.exit_code });
55
+ if (value.type === 'item.completed') send('status', `Process exited with code ${item.exit_code ?? 'unknown'}`, `${id}:exit`);
56
+ }
57
+ if (item.type === 'file_change') send('tool', (item.changes ?? []).map(change => `${change.kind} ${change.path}${change.diff ? `\n${change.diff}` : ''}`).join('\n'), id);
58
+ if (item.type === 'mcp_tool_call') {
59
+ send('tool', `${item.server}/${item.tool}\n${JSON.stringify(item.arguments ?? {}, null, 2)}`, `${id}:call`);
60
+ if (item.result) send('tool-output', JSON.stringify(item.result, null, 2), `${id}:result`);
61
+ if (item.error) send('error', JSON.stringify(item.error), `${id}:error`);
62
+ }
63
+ if (item.type === 'web_search') send('tool', `Search: ${item.query ?? JSON.stringify(item.action ?? {})}`, id);
64
+ if (item.type === 'error') send('error', item.message, id);
65
+ } else if (value.type === 'text' && value.part) {
66
+ send('assistant', value.part.text, value.part.id);
67
+ } else if (value.type === 'tool_use' && value.part) {
68
+ const part = value.part;
69
+ send('tool', `${part.tool}\n${JSON.stringify(part.state?.input ?? {}, null, 2)}`, `${part.id}:call`);
70
+ send('tool-output', part.state?.output, `${part.id}:output`, { status: part.state?.status });
71
+ send('error', part.state?.error, `${part.id}:error`);
72
+ } else if (value.type === 'error' || value.type === 'turn.failed') {
73
+ send('error', value.error?.message ?? value.message ?? JSON.stringify(value.error), value.id);
74
+ } else if (value.type === 'result' && value.is_error) {
75
+ send('error', value.result ?? value.errors?.join('\n'), value.uuid);
76
+ }
77
+ };
78
+ const consume = line => {
79
+ if (!line.trim()) return;
80
+ let value;
81
+ try { value = JSON.parse(line); } catch { send('tool-output', line); return; }
82
+ record(value);
83
+ };
84
+ return {
85
+ record,
86
+ write(chunk) {
87
+ pending += typeof chunk === 'string' ? chunk : decoder.write(chunk);
88
+ let newline;
89
+ while ((newline = pending.indexOf('\n')) >= 0) {
90
+ consume(pending.slice(0, newline));
91
+ pending = pending.slice(newline + 1);
92
+ }
93
+ },
94
+ end() { pending += decoder.end(); if (pending) consume(pending); pending = ''; },
95
+ stderr(chunk) { send('error', String(chunk)); },
96
+ };
97
+ }
@@ -0,0 +1,40 @@
1
+ import { emitKeypressEvents } from 'node:readline';
2
+ import { PassThrough } from 'node:stream';
3
+ import { StringDecoder } from 'node:string_decoder';
4
+
5
+ // Readline handles keyboard keys, but treats SGR mouse reports as typed text.
6
+ // Remove mouse/paste framing before passing keyboard sequences to readline.
7
+ export function terminalKeys(input, onKey) {
8
+ const keys = new PassThrough();
9
+ const decoder = new StringDecoder('utf8');
10
+ emitKeypressEvents(keys); keys.on('keypress', onKey);
11
+ let pending = '', pasted = null, timer;
12
+ const consume = chunk => {
13
+ clearTimeout(timer);
14
+ pending += typeof chunk === 'string' ? chunk : decoder.write(chunk);
15
+ while (pending) {
16
+ if (pasted !== null) {
17
+ const end = pending.indexOf('\x1b[201~');
18
+ if (end < 0) return;
19
+ onKey(pasted + pending.slice(0, end), { name: 'paste' });
20
+ pasted = null; pending = pending.slice(end + 6); continue;
21
+ }
22
+ const escape = pending.indexOf('\x1b');
23
+ if (escape !== 0) {
24
+ const length = escape < 0 ? pending.length : escape;
25
+ keys.write(pending.slice(0, length)); pending = pending.slice(length); continue;
26
+ }
27
+ const sequence = /^(?:\x1b\[[0-?]*[ -/]*[@-~]|\x1bO.|\x1b[^\[O])/s.exec(pending)?.[0];
28
+ if (!sequence) {
29
+ timer = setTimeout(() => { keys.write(pending); pending = ''; }, 50);
30
+ return;
31
+ }
32
+ pending = pending.slice(sequence.length);
33
+ if (sequence === '\x1b[200~') pasted = '';
34
+ else if (/^\x1b\[</.test(sequence)) onKey('', { sequence });
35
+ else keys.write(sequence);
36
+ }
37
+ };
38
+ input.on('data', consume);
39
+ return () => { clearTimeout(timer); input.off('data', consume); keys.off('keypress', onKey); keys.destroy(); };
40
+ }
@@ -0,0 +1,40 @@
1
+ import { createWriteStream } from 'node:fs';
2
+ import { finished } from 'node:stream/promises';
3
+ import { cleanText, STAGES, workflowStage } from './transcript.mjs';
4
+
5
+ // Keep pipes and ordinary terminal scrollback usable, including text deltas.
6
+ export class PlainSession {
7
+ constructor({ output = process.stdout } = {}) {
8
+ this.output = output; this.previous = new Map(); this.initial = [];
9
+ }
10
+ async attach(filename) {
11
+ this.file = createWriteStream(filename, { flags: 'a', mode: 0o600 });
12
+ this.file.on('error', error => { this.fileError = error; });
13
+ for (const event of this.initial) this.file.write(`${JSON.stringify(event)}\n`);
14
+ this.initial = [];
15
+ }
16
+ event(event) {
17
+ const value = { at: new Date().toISOString(), ...event };
18
+ if (this.file && !this.fileError) this.file.write(`${JSON.stringify(value)}\n`);
19
+ else this.initial.push(value);
20
+ if (event.kind === 'diagnostic') return;
21
+ if (event.kind === 'stage') {
22
+ const index = workflowStage(event.stage);
23
+ if (index !== undefined) this.output.write(`\n[${index + 1}/5 ${STAGES[index]}]\n`);
24
+ this.lastId = null;
25
+ return;
26
+ }
27
+ const text = cleanText(event.text ?? '');
28
+ if (!text) return;
29
+ const old = event.id ? this.previous.get(event.id) : null;
30
+ if (text === old) return;
31
+ if (old && text.startsWith(old) && this.lastId === event.id) this.output.write(text.slice(old.length));
32
+ else this.output.write(`\n\n${text}`);
33
+ if (event.id) this.previous.set(event.id, text);
34
+ this.lastId = event.id;
35
+ }
36
+ async close() {
37
+ this.output.write('\n');
38
+ if (this.file) { this.file.end(); await finished(this.file); }
39
+ }
40
+ }
@@ -0,0 +1,22 @@
1
+ import { createRuntimeStream, hasSessionEvents, sessionEvent } from './events.mjs';
2
+
3
+ // Read append-only trace bytes from the user's private staging object. A byte
4
+ // cursor preserves every event between polls, including a partial UTF-8 line.
5
+ export function watchRemoteTranscript({ storage, bucket, object, source, runtime, intervalMs = 2000 }) {
6
+ if (!hasSessionEvents()) return async () => {};
7
+ const stream = createRuntimeStream({ source, runtime });
8
+ let offset = 0; let active = null; let warning = false;
9
+ const poll = async () => {
10
+ try {
11
+ const chunk = await storage.readObjectBytes({ bucket, object, offset });
12
+ if (chunk.length) { stream.write(chunk); offset += chunk.length; }
13
+ warning = false;
14
+ } catch (error) {
15
+ if (!warning) sessionEvent({ kind: 'error', text: `Live output connection interrupted: ${error.message}. Reconnecting…` });
16
+ warning = true;
17
+ }
18
+ };
19
+ const tick = () => { if (!active) active = poll().finally(() => { active = null; }); };
20
+ const timer = setInterval(tick, intervalMs); timer.unref(); tick();
21
+ return async () => { clearInterval(timer); await active; await poll(); stream.end(); };
22
+ }