@openpond/evals 0.11.16 → 0.11.17

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 (158) hide show
  1. package/dist/connected-evidence/contracts.js +72 -0
  2. package/dist/connected-evidence/imports.js +71 -0
  3. package/dist/connected-evidence/index.js +5 -0
  4. package/dist/connected-evidence/normalize.js +97 -0
  5. package/dist/connected-evidence/python-correctness.js +73 -0
  6. package/dist/connected-evidence/sources/claude-code.js +71 -0
  7. package/dist/connected-evidence/sources/codex.js +52 -0
  8. package/dist/connected-evidence/sources/history.js +86 -0
  9. package/dist/connected-evidence/sources/usage.js +15 -0
  10. package/dist/connected-evidence/usage-summary.js +52 -0
  11. package/dist/experiments.js +16 -4
  12. package/dist/graders.js +9 -6
  13. package/dist/human-review/comparison.js +161 -0
  14. package/dist/human-review/contracts.js +110 -0
  15. package/dist/human-review/execution.js +61 -0
  16. package/dist/human-review/handoff.js +18 -0
  17. package/dist/human-review/index.js +9 -0
  18. package/dist/human-review/local-publication.js +18 -0
  19. package/dist/human-review/repository.js +1 -0
  20. package/dist/human-review/results.js +18 -0
  21. package/dist/human-review/service.js +268 -0
  22. package/dist/human-review/task-proposals.js +39 -0
  23. package/dist/index.js +1 -0
  24. package/dist/javascript-verifier-contract.js +2 -2
  25. package/dist/learning/authoring.js +2 -1
  26. package/dist/learning/contracts.js +353 -81
  27. package/dist/learning/index.js +1 -0
  28. package/dist/learning/reward-authoring.js +5 -2
  29. package/dist/learning/training-handoff.js +32 -0
  30. package/dist/metrics-node.js +2 -0
  31. package/dist/profile-evaluation-target.js +7 -0
  32. package/dist/profile-evaluations.js +7 -10
  33. package/dist/profile-external-evaluation.js +35 -0
  34. package/dist/profile-source-candidate.js +5 -0
  35. package/dist/rewards.js +5 -3
  36. package/dist/taskset-run-contract.js +10 -0
  37. package/dist/tasksets.js +2 -0
  38. package/dist/types/builtin-benchmarks/harness-refiner.d.ts +75 -1
  39. package/dist/types/builtin-benchmarks/harness-refiner.d.ts.map +1 -1
  40. package/dist/types/conformance.d.ts +150 -2
  41. package/dist/types/conformance.d.ts.map +1 -1
  42. package/dist/types/connected-evidence/contracts.d.ts +317 -0
  43. package/dist/types/connected-evidence/contracts.d.ts.map +1 -0
  44. package/dist/types/connected-evidence/imports.d.ts +19 -0
  45. package/dist/types/connected-evidence/imports.d.ts.map +1 -0
  46. package/dist/types/connected-evidence/index.d.ts +6 -0
  47. package/dist/types/connected-evidence/index.d.ts.map +1 -0
  48. package/dist/types/connected-evidence/normalize.d.ts +25 -0
  49. package/dist/types/connected-evidence/normalize.d.ts.map +1 -0
  50. package/dist/types/connected-evidence/python-correctness.d.ts +196 -0
  51. package/dist/types/connected-evidence/python-correctness.d.ts.map +1 -0
  52. package/dist/types/connected-evidence/sources/claude-code.d.ts +74 -0
  53. package/dist/types/connected-evidence/sources/claude-code.d.ts.map +1 -0
  54. package/dist/types/connected-evidence/sources/codex.d.ts +74 -0
  55. package/dist/types/connected-evidence/sources/codex.d.ts.map +1 -0
  56. package/dist/types/connected-evidence/sources/history.d.ts +146 -0
  57. package/dist/types/connected-evidence/sources/history.d.ts.map +1 -0
  58. package/dist/types/connected-evidence/sources/usage.d.ts +3 -0
  59. package/dist/types/connected-evidence/sources/usage.d.ts.map +1 -0
  60. package/dist/types/connected-evidence/usage-summary.d.ts +162 -0
  61. package/dist/types/connected-evidence/usage-summary.d.ts.map +1 -0
  62. package/dist/types/evidence/conformance.d.ts +28 -28
  63. package/dist/types/evidence/contracts.d.ts +27 -27
  64. package/dist/types/execution-contracts.d.ts +159 -5
  65. package/dist/types/execution-contracts.d.ts.map +1 -1
  66. package/dist/types/experiments.d.ts +476 -7
  67. package/dist/types/experiments.d.ts.map +1 -1
  68. package/dist/types/graders.d.ts +21 -1
  69. package/dist/types/graders.d.ts.map +1 -1
  70. package/dist/types/human-review/comparison.d.ts +1959 -0
  71. package/dist/types/human-review/comparison.d.ts.map +1 -0
  72. package/dist/types/human-review/contracts.d.ts +2719 -0
  73. package/dist/types/human-review/contracts.d.ts.map +1 -0
  74. package/dist/types/human-review/execution.d.ts +298 -0
  75. package/dist/types/human-review/execution.d.ts.map +1 -0
  76. package/dist/types/human-review/handoff.d.ts +124 -0
  77. package/dist/types/human-review/handoff.d.ts.map +1 -0
  78. package/dist/types/human-review/index.d.ts +10 -0
  79. package/dist/types/human-review/index.d.ts.map +1 -0
  80. package/dist/types/human-review/local-publication.d.ts +165 -0
  81. package/dist/types/human-review/local-publication.d.ts.map +1 -0
  82. package/dist/types/human-review/repository.d.ts +43 -0
  83. package/dist/types/human-review/repository.d.ts.map +1 -0
  84. package/dist/types/human-review/results.d.ts +118 -0
  85. package/dist/types/human-review/results.d.ts.map +1 -0
  86. package/dist/types/human-review/service.d.ts +878 -0
  87. package/dist/types/human-review/service.d.ts.map +1 -0
  88. package/dist/types/human-review/task-proposals.d.ts +70 -0
  89. package/dist/types/human-review/task-proposals.d.ts.map +1 -0
  90. package/dist/types/index.d.ts +1 -0
  91. package/dist/types/index.d.ts.map +1 -1
  92. package/dist/types/javascript-environment-contract.d.ts +1 -1
  93. package/dist/types/javascript-verifier-contract.d.ts +1 -1
  94. package/dist/types/javascript-verifier-contract.d.ts.map +1 -1
  95. package/dist/types/learned-preference.d.ts +2 -2
  96. package/dist/types/learning/acceptance-group.d.ts +13 -13
  97. package/dist/types/learning/acceptance-plan.d.ts +1 -1
  98. package/dist/types/learning/admission.d.ts +78 -1
  99. package/dist/types/learning/admission.d.ts.map +1 -1
  100. package/dist/types/learning/authoring-service.d.ts +2 -0
  101. package/dist/types/learning/authoring-service.d.ts.map +1 -1
  102. package/dist/types/learning/authoring.d.ts +20 -0
  103. package/dist/types/learning/authoring.d.ts.map +1 -1
  104. package/dist/types/learning/batch-service.d.ts +76 -2
  105. package/dist/types/learning/batch-service.d.ts.map +1 -1
  106. package/dist/types/learning/contracts.d.ts +759 -24
  107. package/dist/types/learning/contracts.d.ts.map +1 -1
  108. package/dist/types/learning/index.d.ts +1 -0
  109. package/dist/types/learning/index.d.ts.map +1 -1
  110. package/dist/types/learning/iteration-candidate-decision.d.ts +2 -2
  111. package/dist/types/learning/iteration-dispatch-contracts.d.ts +2 -2
  112. package/dist/types/learning/operations.d.ts +1118 -8
  113. package/dist/types/learning/operations.d.ts.map +1 -1
  114. package/dist/types/learning/repository.d.ts +402 -15
  115. package/dist/types/learning/repository.d.ts.map +1 -1
  116. package/dist/types/learning/reward-authoring.d.ts.map +1 -1
  117. package/dist/types/learning/reward-calibration.d.ts +74 -0
  118. package/dist/types/learning/reward-calibration.d.ts.map +1 -1
  119. package/dist/types/learning/reward-checks.d.ts +14 -2
  120. package/dist/types/learning/reward-checks.d.ts.map +1 -1
  121. package/dist/types/learning/training-handoff.d.ts +201 -0
  122. package/dist/types/learning/training-handoff.d.ts.map +1 -0
  123. package/dist/types/learning/transport.d.ts +963 -21
  124. package/dist/types/learning/transport.d.ts.map +1 -1
  125. package/dist/types/metrics-node.d.ts +3 -1
  126. package/dist/types/metrics-node.d.ts.map +1 -1
  127. package/dist/types/model-improvement-qualification.d.ts +3 -3
  128. package/dist/types/native-harness-experiment-attempt.d.ts +2 -2
  129. package/dist/types/profile-evaluation-comparisons.d.ts +230 -0
  130. package/dist/types/profile-evaluation-comparisons.d.ts.map +1 -1
  131. package/dist/types/profile-evaluation-target.d.ts +14 -0
  132. package/dist/types/profile-evaluation-target.d.ts.map +1 -0
  133. package/dist/types/profile-evaluations.d.ts +117 -14
  134. package/dist/types/profile-evaluations.d.ts.map +1 -1
  135. package/dist/types/profile-external-evaluation.d.ts +404 -0
  136. package/dist/types/profile-external-evaluation.d.ts.map +1 -0
  137. package/dist/types/profile-source-candidate.d.ts +15 -0
  138. package/dist/types/profile-source-candidate.d.ts.map +1 -0
  139. package/dist/types/review-conformance.d.ts +7 -7
  140. package/dist/types/rewards.d.ts +253 -0
  141. package/dist/types/rewards.d.ts.map +1 -1
  142. package/dist/types/rollouts.d.ts +3 -3
  143. package/dist/types/taskset-run-contract.d.ts +230 -0
  144. package/dist/types/taskset-run-contract.d.ts.map +1 -1
  145. package/dist/types/tasksets.d.ts +311 -3
  146. package/dist/types/tasksets.d.ts.map +1 -1
  147. package/dist/types/telemetry-analysis.d.ts +2 -2
  148. package/dist/types/telemetry-bundle.d.ts +6 -6
  149. package/dist/types/telemetry.d.ts +3 -3
  150. package/package.json +9 -1
  151. package/schemas/learning/v1/command-request.schema.json +1745 -409
  152. package/schemas/learning/v1/decision.schema.json +11 -0
  153. package/schemas/learning/v1/draft.schema.json +11 -0
  154. package/schemas/learning/v1/grade.schema.json +11 -0
  155. package/schemas/learning/v1/package.schema.json +532 -12
  156. package/schemas/learning/v1/policy.schema.json +836 -28
  157. package/schemas/learning/v1/reward.schema.json +520 -0
  158. package/schemas/learning/v1/reward_check.schema.json +9 -0
@@ -0,0 +1,72 @@
1
+ import { z } from "zod";
2
+ import { contentHash } from "@openpond/harness";
3
+ export const CONNECTED_EVIDENCE_VERSION = "openpond.connectedEvidence.v1";
4
+ export const CONNECTED_NORMALIZER_VERSION = "connected-evidence-1";
5
+ export const CONNECTED_EVIDENCE_LIMITS = { files: 100, sourceBytes: 32 * 1024 * 1024, decodedBytes: 64 * 1024 * 1024,
6
+ lineBytes: 2 * 1024 * 1024, events: 100_000, sessions: 1_000, boundaries: 5_000, evaluatorBytes: 2 * 1024 * 1024 };
7
+ const Id = z.string().min(1).max(500);
8
+ const Hash = z.string().regex(/^[a-f0-9]{64}$/);
9
+ const Time = z.string().datetime().nullable();
10
+ const Tokens = z.number().int().nonnegative().nullable();
11
+ export const ConnectedSourceKindSchema = z.enum(["native_chat", "native_work", "codex", "claude_code", "hermes", "openclaw"]);
12
+ export const ConnectedUsageSchema = z.object({
13
+ invocationId: Id, inputTokens: Tokens, outputTokens: Tokens, cachedInputTokens: Tokens,
14
+ totalTokens: Tokens, provenance: z.enum(["billed_invocation", "reported_invocation", "reported_cumulative"]),
15
+ }).strict();
16
+ export const ConnectedEventSchema = z.object({
17
+ id: Id, sequence: z.number().int().nonnegative(), occurredAt: Time,
18
+ kind: z.enum(["message", "tool_call", "tool_result", "compaction", "artifact", "terminal", "usage", "unknown"]),
19
+ role: z.enum(["user", "assistant", "system", "tool"]).nullable(),
20
+ callId: Id.nullable(), parentId: Id.nullable(), content: z.json(),
21
+ usage: ConnectedUsageSchema.nullable(),
22
+ }).strict();
23
+ export const ConnectedBoundarySchema = z.object({
24
+ id: Id, familyKey: Id, projection: z.enum(["turn", "conversation"]),
25
+ requestEventId: Id, start: z.number().int().nonnegative(), end: z.number().int().positive(),
26
+ inputHash: Hash, outputHash: Hash.nullable(), revisionHash: Hash,
27
+ terminal: z.enum(["completed", "failed", "cancelled", "unknown"]),
28
+ coverage: z.object({ answer: z.boolean(), context: z.enum(["retained", "compacted", "unknown"]),
29
+ process: z.enum(["retained", "partial", "absent"]), artifacts: z.enum(["retained", "references_only", "absent"]),
30
+ replay: z.literal("unavailable"), gaps: z.array(z.string().max(500)).max(100) }).strict(),
31
+ }).strict();
32
+ export const ConnectedSessionSchema = z.object({
33
+ schemaVersion: z.literal(CONNECTED_EVIDENCE_VERSION), normalizerVersion: z.literal(CONNECTED_NORMALIZER_VERSION),
34
+ origin: ConnectedSourceKindSchema, sessionId: Id, branchId: Id.nullable(), parentSessionId: Id.nullable(),
35
+ exporterVersion: z.string().max(200).nullable(),
36
+ sourceFiles: z.array(z.object({ path: Id, contentHash: Hash, sizeBytes: z.number().int().nonnegative() }).strict()).max(CONNECTED_EVIDENCE_LIMITS.files),
37
+ events: z.array(ConnectedEventSchema).max(CONNECTED_EVIDENCE_LIMITS.events),
38
+ unmappedEvents: z.array(ConnectedEventSchema).max(CONNECTED_EVIDENCE_LIMITS.events),
39
+ boundaries: z.array(ConnectedBoundarySchema).max(CONNECTED_EVIDENCE_LIMITS.boundaries),
40
+ warnings: z.array(z.string().max(500)).max(100), contentHash: Hash,
41
+ }).strict();
42
+ export function hasRecordedConnectedAnswer(event) {
43
+ if (event.kind !== "message" || event.role !== "assistant" || event.content === null)
44
+ return false;
45
+ if (typeof event.content === "string")
46
+ return event.content.trim().length > 0;
47
+ if (Array.isArray(event.content))
48
+ return event.content.length > 0;
49
+ return typeof event.content !== "object" || !("text" in event.content) || typeof event.content.text === "string" && event.content.text.trim().length > 0;
50
+ }
51
+ /** Bytes stay in one immutable snapshot; cases retain cutoffs, not copied history. */
52
+ export function resolveConnectedBoundary(sessionValue, boundaryId) {
53
+ const session = ConnectedSessionSchema.parse(sessionValue);
54
+ const { contentHash: actual, ...body } = session;
55
+ if (contentHash(body) !== actual)
56
+ throw new Error("connected_evidence_hash_mismatch");
57
+ const boundary = session.boundaries.find(item => item.id === boundaryId);
58
+ if (!boundary)
59
+ throw new Error("connected_boundary_missing");
60
+ const input = session.events.slice(0, boundary.start + 1);
61
+ const observed = session.events.slice(boundary.start + 1, boundary.end);
62
+ if (contentHash(input) !== boundary.inputHash || contentHash(observed) !== boundary.revisionHash)
63
+ throw new Error("connected_boundary_hash_mismatch");
64
+ const answers = observed.filter(hasRecordedConnectedAnswer);
65
+ const answer = boundary.projection === "conversation" ? answers.map(event => event.content) : answers.at(-1)?.content ?? null;
66
+ if ((answers.length ? contentHash(answer) : null) !== boundary.outputHash)
67
+ throw new Error("connected_output_hash_mismatch");
68
+ const value = { input, observed, answer, boundary };
69
+ if (new TextEncoder().encode(JSON.stringify(value)).length > CONNECTED_EVIDENCE_LIMITS.evaluatorBytes)
70
+ throw new Error("connected_evaluator_context_too_large");
71
+ return value;
72
+ }
@@ -0,0 +1,71 @@
1
+ import { contentHash } from "@openpond/harness";
2
+ import { CONNECTED_EVIDENCE_LIMITS } from "./contracts.js";
3
+ import { connectedJsonLines } from "./normalize.js";
4
+ import { parseCodexSession } from "./sources/codex.js";
5
+ import { parseClaudeSession } from "./sources/claude-code.js";
6
+ import { parseHermesEvidence, parseOpenClawEvidence } from "./sources/history.js";
7
+ /** Never admits malformed files partly or executes referenced paths/commands. */
8
+ export function previewAgentImport(input) {
9
+ if (!input.files.length || input.files.length > CONNECTED_EVIDENCE_LIMITS.files)
10
+ throw new Error("Choose between 1 and 100 source files.");
11
+ const paths = new Set();
12
+ let bytes = 0;
13
+ for (const file of input.files) {
14
+ if (file.path.length > 500 || file.path.startsWith("/") || file.path.includes("\\") || file.path.split("/").some(part => !part || part === "." || part === "..") || paths.has(file.path))
15
+ throw new Error("Unsafe or duplicate source file path.");
16
+ paths.add(file.path);
17
+ bytes += new TextEncoder().encode(file.text).length;
18
+ }
19
+ if (bytes > CONNECTED_EVIDENCE_LIMITS.decodedBytes)
20
+ throw new Error("Decoded source files exceed 64 MiB.");
21
+ const sessions = [], issues = [];
22
+ const attempt = (file, parse) => {
23
+ try {
24
+ sessions.push(...parse());
25
+ }
26
+ catch (error) {
27
+ issues.push({ file, message: error instanceof Error ? error.message : "Unsupported session." });
28
+ }
29
+ };
30
+ if (input.source === "openclaw")
31
+ attempt("manifest.json", () => {
32
+ const root = input.files[0].path.split("/")[0];
33
+ const files = !input.files.some(file => file.path === "manifest.json") && input.files.every(file => file.path.startsWith(`${root}/`))
34
+ ? input.files.map(file => ({ ...file, path: file.path.slice(root.length + 1) })) : input.files;
35
+ return [parseOpenClawEvidence(files)];
36
+ });
37
+ else
38
+ for (const file of input.files)
39
+ attempt(file.path, () => {
40
+ let rows;
41
+ if (input.source === "hermes" && file.path.endsWith(".json")) {
42
+ const value = JSON.parse(file.text.replace(/^\uFEFF/u, ""));
43
+ if (!value || typeof value !== "object" || Array.isArray(value))
44
+ throw new Error("Hermes session backup must be one JSON object.");
45
+ rows = [value];
46
+ }
47
+ else
48
+ rows = connectedJsonLines(file);
49
+ if (input.source === "codex")
50
+ return [parseCodexSession(file, rows)];
51
+ if (input.source === "claude_code")
52
+ return [parseClaudeSession(file, rows, input.branchLeafId)];
53
+ if (input.source === "hermes")
54
+ return rows.map(row => parseHermesEvidence(file, row));
55
+ throw new Error("Unsupported source adapter.");
56
+ });
57
+ if (sessions.length > CONNECTED_EVIDENCE_LIMITS.sessions || sessions.reduce((total, session) => total + session.events.length + session.unmappedEvents.length, 0) > CONNECTED_EVIDENCE_LIMITS.events
58
+ || sessions.reduce((total, session) => total + session.boundaries.length, 0) > CONNECTED_EVIDENCE_LIMITS.boundaries)
59
+ throw new Error("Import exceeds the session/event/boundary limit.");
60
+ const identities = new Map();
61
+ for (const session of sessions) {
62
+ const key = JSON.stringify([session.origin, session.sessionId, session.branchId]);
63
+ const prior = identities.get(key);
64
+ if (prior && prior !== session.contentHash)
65
+ throw new Error("The same session has different contents in this selection. Choose one export.");
66
+ identities.set(key, session.contentHash);
67
+ }
68
+ return { schemaVersion: "openpond.agentImportPreview.v1", source: input.source,
69
+ sourceHash: contentHash({ source: input.source, files: [...input.files].sort((a, b) => a.path.localeCompare(b.path)), branchLeafId: input.branchLeafId ?? null }),
70
+ sessions: sessions.filter((session, index) => sessions.findIndex(other => other.contentHash === session.contentHash) === index), issues };
71
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./contracts.js";
2
+ export * from "./normalize.js";
3
+ export * from "./imports.js";
4
+ export * from "./usage-summary.js";
5
+ export * from "./python-correctness.js";
@@ -0,0 +1,97 @@
1
+ import { contentHash } from "@openpond/harness";
2
+ import { CONNECTED_EVIDENCE_LIMITS, CONNECTED_EVIDENCE_VERSION, CONNECTED_NORMALIZER_VERSION, ConnectedSessionSchema, hasRecordedConnectedAnswer } from "./contracts.js";
3
+ export function connectedTimestamp(value, units = "milliseconds") {
4
+ if (typeof value !== "number" && typeof value !== "string")
5
+ return null;
6
+ const time = new Date(typeof value === "number" && units === "seconds" ? value * 1_000 : value);
7
+ return Number.isFinite(time.getTime()) ? time.toISOString() : null;
8
+ }
9
+ export function connectedFileIdentity(file) {
10
+ return { path: file.path, contentHash: contentHash(file.text), sizeBytes: new TextEncoder().encode(file.text).length };
11
+ }
12
+ export function normalizeConnectedSession(input) {
13
+ if (input.events.length + (input.unmappedEvents?.length ?? 0) > CONNECTED_EVIDENCE_LIMITS.events)
14
+ throw new Error("connected_event_limit");
15
+ const events = input.events.map((event, sequence) => ({ ...event, sequence }));
16
+ if (new Set(events.map(event => event.id)).size !== events.length)
17
+ throw new Error("connected_duplicate_event");
18
+ const starts = events.flatMap((event, index) => event.kind === "message" && event.role === "user" ? [index] : []);
19
+ if (!starts.length)
20
+ throw new Error("The session has no retained user request.");
21
+ if (starts.length + (input.includeConversation === false ? 0 : 1) > CONNECTED_EVIDENCE_LIMITS.boundaries)
22
+ throw new Error("connected_boundary_limit");
23
+ const familyKey = `family-${contentHash([input.origin, input.parentSessionId ?? input.sessionId]).slice(0, 32)}`;
24
+ const boundaries = starts.map((start, position) => {
25
+ const end = starts[position + 1] ?? events.length;
26
+ const observed = events.slice(start + 1, end);
27
+ const answer = observed.filter(hasRecordedConnectedAnswer).at(-1);
28
+ const tools = observed.filter(event => event.kind === "tool_call" || event.kind === "tool_result");
29
+ const calls = new Set(tools.filter(event => event.kind === "tool_call").map(event => event.callId));
30
+ const results = new Set(tools.filter(event => event.kind === "tool_result").map(event => event.callId));
31
+ const incomplete = tools.some(event => !event.callId) || [...calls].some(id => !results.has(id)) || [...results].some(id => !calls.has(id));
32
+ const terminal = [...observed].reverse().find(event => event.kind === "terminal");
33
+ const status = terminal && typeof terminal.content === "object" && terminal.content !== null && !Array.isArray(terminal.content)
34
+ ? terminal.content.status : null;
35
+ const compacted = events.slice(0, start + 1).some(event => event.kind === "compaction");
36
+ const unknown = observed.some(event => event.kind === "unknown");
37
+ return {
38
+ id: `case-${contentHash([input.origin, input.sessionId, input.branchId ?? null, events[start].id]).slice(0, 40)}`,
39
+ familyKey, projection: "turn", requestEventId: events[start].id, start, end,
40
+ inputHash: contentHash(events.slice(0, start + 1)), outputHash: answer ? contentHash(answer.content) : null,
41
+ revisionHash: contentHash(observed), terminal: status === "completed" || status === "failed" || status === "cancelled" ? status : "unknown",
42
+ coverage: { answer: Boolean(answer), context: input.contextComplete === false ? "unknown" : compacted ? "compacted" : "retained",
43
+ process: !tools.length ? unknown ? "partial" : "absent" : incomplete || unknown ? "partial" : "retained",
44
+ artifacts: observed.some(event => event.kind === "artifact") ? "references_only" : "absent", replay: "unavailable",
45
+ gaps: [...(!answer ? ["No recorded assistant answer."] : []), ...(input.contextComplete === false ? ["The source omits preceding context."] : []), ...(compacted ? ["Earlier context was compacted by the source."] : []),
46
+ ...(incomplete ? ["Tool call/result correlation is incomplete."] : []), ...(unknown ? ["Some source events have no supported process mapping."] : []),
47
+ ...(!terminal ? ["No terminal receipt was recorded."] : []), "No resettable execution environment was admitted."] },
48
+ };
49
+ });
50
+ if (input.includeConversation !== false) {
51
+ const first = boundaries[0], last = boundaries.at(-1);
52
+ const observed = events.slice(first.start + 1), answers = observed.filter(hasRecordedConnectedAnswer);
53
+ boundaries.push({ ...first, id: `conversation-${contentHash([input.origin, input.sessionId, input.branchId ?? null]).slice(0, 40)}`,
54
+ projection: "conversation", end: events.length, outputHash: answers.length ? contentHash(answers.map(event => event.content)) : null,
55
+ revisionHash: contentHash(observed), terminal: last.terminal, coverage: { ...first.coverage,
56
+ answer: answers.length > 0, process: boundaries.some(item => item.coverage.process === "partial") ? "partial" : boundaries.some(item => item.coverage.process === "retained") ? "retained" : "absent",
57
+ context: boundaries.some(item => item.coverage.context === "unknown") ? "unknown" : boundaries.some(item => item.coverage.context === "compacted") ? "compacted" : "retained",
58
+ artifacts: boundaries.some(item => item.coverage.artifacts === "references_only") ? "references_only" : "absent",
59
+ gaps: [...new Set(boundaries.flatMap(item => item.coverage.gaps))].slice(0, 100) } });
60
+ }
61
+ const body = { schemaVersion: CONNECTED_EVIDENCE_VERSION, normalizerVersion: CONNECTED_NORMALIZER_VERSION,
62
+ origin: input.origin, sessionId: input.sessionId, branchId: input.branchId ?? null, parentSessionId: input.parentSessionId ?? null,
63
+ exporterVersion: input.exporterVersion ?? null, sourceFiles: input.files.map(connectedFileIdentity).sort((a, b) => a.path.localeCompare(b.path)),
64
+ events, unmappedEvents: (input.unmappedEvents ?? []).map((event, sequence) => ({ ...event, sequence })), boundaries, warnings: input.warnings ?? [] };
65
+ const session = ConnectedSessionSchema.parse({ ...body, contentHash: contentHash(body) });
66
+ if (new TextEncoder().encode(JSON.stringify(session)).length > CONNECTED_EVIDENCE_LIMITS.decodedBytes)
67
+ throw new Error("connected_normalized_size_limit");
68
+ return session;
69
+ }
70
+ export function connectedJsonLines(file) {
71
+ const lines = file.text.replace(/^\uFEFF/u, "").split(/\r?\n/u);
72
+ if (lines.length > CONNECTED_EVIDENCE_LIMITS.events + 1)
73
+ throw new Error("connected_event_limit");
74
+ return lines.flatMap((line, index) => {
75
+ if (!line.trim())
76
+ return [];
77
+ if (new TextEncoder().encode(line).length > CONNECTED_EVIDENCE_LIMITS.lineBytes)
78
+ throw new Error(`Source line ${index + 1} exceeds the limit.`);
79
+ let value;
80
+ try {
81
+ value = JSON.parse(line);
82
+ }
83
+ catch {
84
+ throw new Error(`Invalid JSON on line ${index + 1}.`);
85
+ }
86
+ if (!value || typeof value !== "object" || Array.isArray(value))
87
+ throw new Error(`Unsupported record on line ${index + 1}.`);
88
+ return [value];
89
+ });
90
+ }
91
+ export function object(value) {
92
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
93
+ }
94
+ export function event(input) {
95
+ return { sequence: 0, occurredAt: null, role: null, callId: null, parentId: null, usage: null, ...input };
96
+ }
97
+ export function json(value) { return JSON.parse(JSON.stringify(value ?? null)); }
@@ -0,0 +1,73 @@
1
+ import { createRewardRelease } from "../rewards.js";
2
+ import { createLearningTextAsset } from "../learning/assets.js";
3
+ export const PYTHON_CORRECTNESS_PRESET_VERSION = "python-stdio-correctness-1";
4
+ export const PYTHON_CORRECTNESS_DESCRIPTION = "Check explicitly admitted Python programs against owner-authored private stdin/stdout cases. Requires a pinned Python binary and fresh network, filesystem and process isolation. Missing code, tests or qualified runtime is unscorable; syntax alone is not correctness.";
5
+ /** This module runs only in an owner-admitted sandbox_process grader. Python -I
6
+ * isolates Python configuration; bubblewrap separately isolates untrusted code.
7
+ * The candidate receives each test input, never expected outputs or grader files. */
8
+ export const PYTHON_CORRECTNESS_VERIFIER_SOURCE = String.raw `
9
+ import { spawn } from "node:child_process";
10
+ import { mkdtemp, writeFile, readFile, rm } from "node:fs/promises";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+ import { createHash } from "node:crypto";
14
+ const unavailable = feedback => ({ score: null, passed: false, feedback, evidenceRefs: [] });
15
+ async function run(args, stdin = "", milliseconds = 2000) {
16
+ return await new Promise((resolve, reject) => {
17
+ const child = spawn("/usr/bin/bwrap", args, { detached: true, env: {}, stdio: ["pipe", "pipe", "pipe"] });
18
+ let chunks = [], bytes = 0, overflow = false, timedOut = false, settled = false;
19
+ const kill = () => { if (child.pid) try { process.kill(-child.pid, "SIGKILL"); } catch (error) { if (error.code !== "ESRCH") throw error; } };
20
+ const timer = setTimeout(() => { timedOut = true; kill(); }, milliseconds);
21
+ const add = (data, retain) => { bytes += data.length; if (bytes > 65536) { overflow = true; kill(); } else if (retain) chunks.push(data); };
22
+ child.stdout.on("data", data => add(data, true)); child.stderr.on("data", data => add(data, false));
23
+ child.stdin.on("error", () => {});
24
+ child.once("error", () => { if (!settled) { settled = true; clearTimeout(timer); reject(new Error("python_correctness_isolation_unavailable")); } });
25
+ child.once("close", code => { clearTimeout(timer); kill(); if (!settled) { settled = true; resolve({ code, stdout: Buffer.concat(chunks).toString("utf8"), overflow, timedOut }); } });
26
+ child.stdin.end(stdin);
27
+ });
28
+ }
29
+ const isolation = ["--unshare-all", "--die-with-parent", "--new-session", "--cap-drop", "ALL", "--ro-bind", "/usr", "/usr", "--ro-bind", "/lib", "/lib", "--ro-bind", "/lib64", "/lib64", "--proc", "/proc", "--dev", "/dev", "--tmpfs", "/tmp", "--tmpfs", "/case", "--clearenv", "--setenv", "PATH", "/usr/bin", "--chdir", "/case"];
30
+ export async function verify({ task, output, expectedOutput }) {
31
+ const code = output?.pythonCode ?? output?.answer?.pythonCode;
32
+ const criteria = expectedOutput?.pythonCorrectness;
33
+ if (typeof code !== "string" || !code.trim() || Buffer.byteLength(code) > 65536) return unavailable("Python correctness requires a retained explicit pythonCode artifact of at most 64 KiB.");
34
+ if (!criteria || criteria.schemaVersion !== "openpond.pythonStdioTests.v1" || !Array.isArray(criteria.tests) || !criteria.tests.length || criteria.tests.length > 20
35
+ || criteria.tests.some(test => !test || typeof test.stdin !== "string" || typeof test.stdout !== "string" || Buffer.byteLength(test.stdin) > 65536 || Buffer.byteLength(test.stdout) > 65536))
36
+ return unavailable("Python correctness requires owner-authored private stdin/stdout tests; a captured answer is not a reference.");
37
+ const admission = criteria.runtime;
38
+ if (!admission || admission.runtime !== "bubblewrap-python-stdio-v1" || !/^[a-f0-9]{64}$/.test(admission.pythonSha256 ?? ""))
39
+ return unavailable("An owner-admitted pinned isolated Python runtime is required.");
40
+ try {
41
+ const binaryHash = createHash("sha256").update(await readFile("/usr/bin/python3")).digest("hex");
42
+ if (binaryHash !== admission.pythonSha256) return unavailable("The admitted Python binary changed; qualify and pin the current runtime.");
43
+ const probe = await run([...isolation, "--", "/usr/bin/python3", "-I", "-S", "-c", "import sys; print(sys.version_info.major)"]);
44
+ if (probe.code !== 0 || probe.stdout.trim() !== "3" || probe.overflow || probe.timedOut) return unavailable("Python namespace isolation is unavailable in this runtime.");
45
+ } catch { return unavailable("The pinned Python isolation runtime is unavailable."); }
46
+ const directory = await mkdtemp(join(tmpdir(), "openpond-python-correctness-"));
47
+ let passed = 0;
48
+ try {
49
+ const source = join(directory, "main.py"); await writeFile(source, code, { mode: 0o400 });
50
+ for (const test of criteria.tests) {
51
+ const result = await run([...isolation, "--ro-bind", source, "/case/main.py", "--", "/usr/bin/prlimit", "--as=268435456", "--cpu=2", "--fsize=65536", "--", "/usr/bin/python3", "-I", "-S", "/case/main.py"], test.stdin);
52
+ if (result.code === 0 && !result.overflow && !result.timedOut && result.stdout === test.stdout) passed++;
53
+ }
54
+ return { score: passed / criteria.tests.length, passed: passed === criteria.tests.length,
55
+ feedback: "Python correctness: " + passed + " of " + criteria.tests.length + " private tests passed.", evidenceRefs: [] };
56
+ } finally { await rm(directory, { recursive: true, force: true }); }
57
+ }
58
+ `;
59
+ export function pythonCorrectnessAuthoringFields() {
60
+ return { kind: "custom_verifier", name: "Python correctness", description: PYTHON_CORRECTNESS_DESCRIPTION,
61
+ feedbackKey: "python_correctness", code: PYTHON_CORRECTNESS_VERIFIER_SOURCE, exportName: "verify", timeout: "60000", verifierRuntime: "sandbox_process" };
62
+ }
63
+ /** Explicit owner publication may import these genuine immutable assets like
64
+ * any other supplied grader. No publication, compute or capture happens here. */
65
+ export function createPythonCorrectnessPreset() {
66
+ const asset = createLearningTextAsset({ text: PYTHON_CORRECTNESS_VERIFIER_SOURCE,
67
+ path: "graders/openpond-python-stdio-correctness-v1.mjs", mediaType: "application/javascript", visibility: "verifier" });
68
+ const reward = createRewardRelease({ schemaVersion: "openpond.rewardRelease.v1", id: "openpond-python-stdio-correctness", revision: 1,
69
+ name: "Python correctness", description: PYTHON_CORRECTNESS_DESCRIPTION, feedbackKey: "python_correctness",
70
+ implementation: { kind: "custom_verifier", runtime: "sandbox_process", verifierRef: asset.asset, exportName: "verify", timeoutMs: 60_000, networkPolicy: "none" },
71
+ rawScore: { minimum: 0, maximum: 1 }, assets: [asset.asset] });
72
+ return { reward, assets: [asset] };
73
+ }
@@ -0,0 +1,71 @@
1
+ import { contentHash } from "@openpond/harness";
2
+ import { connectedTimestamp, event, json, normalizeConnectedSession, object } from "../normalize.js";
3
+ import { sourceUsage } from "./usage.js";
4
+ export function parseClaudeSession(file, rows, leafId) {
5
+ const messages = rows.filter(row => (row.type === "user" || row.type === "assistant") && typeof row.uuid === "string");
6
+ const sessionIds = new Set(messages.flatMap(row => typeof row.sessionId === "string" ? [row.sessionId] : []));
7
+ if (sessionIds.size !== 1 || !messages.length)
8
+ throw new Error("A supported Claude Code transcript must identify one session with UUID messages.");
9
+ const byId = new Map(messages.map(row => [row.uuid, row]));
10
+ if (byId.size !== messages.length)
11
+ throw new Error("Claude Code transcript contains duplicate message UUIDs.");
12
+ const parents = new Set(messages.flatMap(row => typeof row.parentUuid === "string" ? [row.parentUuid] : []));
13
+ const leaves = messages.filter(row => !parents.has(row.uuid));
14
+ if (!leafId && leaves.length !== 1)
15
+ throw new Error("This transcript has multiple branches. Select a branch leaf UUID before importing.");
16
+ let current = byId.get(leafId ?? leaves[0].uuid);
17
+ if (!current)
18
+ throw new Error("The selected Claude Code branch leaf is missing.");
19
+ const chain = [], seen = new Set();
20
+ let missingParent = false;
21
+ while (current) {
22
+ const id = current.uuid;
23
+ if (seen.has(id))
24
+ throw new Error("Claude Code branch contains a parent cycle.");
25
+ seen.add(id);
26
+ chain.unshift(current);
27
+ const parent = current.parentUuid;
28
+ if (typeof parent !== "string")
29
+ break;
30
+ current = byId.get(parent);
31
+ if (!current)
32
+ missingParent = true;
33
+ }
34
+ const events = [];
35
+ for (const row of chain) {
36
+ const message = object(row.message), content = message.content;
37
+ const base = { occurredAt: connectedTimestamp(row.timestamp), parentId: typeof row.parentUuid === "string" ? row.parentUuid : null };
38
+ const role = row.type === "user" ? "user" : "assistant";
39
+ const blocks = Array.isArray(content) ? content : [{ type: "text", text: content }];
40
+ const textBlocks = blocks.filter(block => object(block).type === "text");
41
+ // User tool_result messages are process evidence, not independent requests.
42
+ if (textBlocks.length)
43
+ events.push(event({ ...base, id: row.uuid, kind: "message", role, content: json(textBlocks) }));
44
+ if (role === "assistant" && message.usage)
45
+ events.push(event({ ...base, id: `${row.uuid}:usage`, kind: "usage", content: json(message.usage),
46
+ usage: sourceUsage(message.usage, typeof message.id === "string" ? message.id : row.uuid) }));
47
+ for (const [index, value] of blocks.entries()) {
48
+ const block = object(value);
49
+ if (block.type === "text" || block.type === "thinking" || block.type === "redacted_thinking")
50
+ continue;
51
+ const id = `${row.uuid}:${index}`;
52
+ if (block.type === "tool_use")
53
+ events.push(event({ ...base, id, kind: "tool_call", callId: typeof block.id === "string" ? block.id : null, content: json(block) }));
54
+ else if (block.type === "tool_result")
55
+ events.push(event({ ...base, id, kind: "tool_result", role: "tool", callId: typeof block.tool_use_id === "string" ? block.tool_use_id : null, content: json(block) }));
56
+ else
57
+ events.push(event({ ...base, id, kind: "unknown", content: json(block) }));
58
+ }
59
+ if (message.stop_reason === "end_turn")
60
+ events.push(event({ ...base, id: `${row.uuid}:terminal`, kind: "terminal", content: { status: "completed" } }));
61
+ }
62
+ if (rows.some(row => row.type === "system" && row.subtype === "compact_boundary"))
63
+ events.unshift(event({ id: `compaction-${contentHash(rows.filter(row => row.subtype === "compact_boundary")).slice(0, 32)}`, kind: "compaction", content: { source: "claude_compact_boundary" } }));
64
+ return normalizeConnectedSession({ origin: "claude_code", sessionId: [...sessionIds][0],
65
+ // A chosen leaf is a cutoff, not a new family or the identity of every turn.
66
+ branchId: null, parentSessionId: typeof chain[0]?.parentSessionId === "string" ? chain[0].parentSessionId : null,
67
+ exporterVersion: typeof chain.at(-1)?.version === "string" ? chain.at(-1).version : null,
68
+ files: [file], events, contextComplete: !missingParent, warnings: [...(missingParent ? ["The transcript omits an ancestor; complete context is unavailable."] : []),
69
+ ...(chain.some(row => row.isSidechain) ? ["This is a subagent/sidechain transcript; parent context is not implicitly imported."] : []),
70
+ "Claude Code transcript schema is internal; only the declared message shapes are normalized."] });
71
+ }
@@ -0,0 +1,52 @@
1
+ import { contentHash } from "@openpond/harness";
2
+ import { connectedTimestamp, event, json, normalizeConnectedSession, object } from "../normalize.js";
3
+ import { sourceUsage } from "./usage.js";
4
+ export function parseCodexSession(file, rows) {
5
+ const metadata = object(rows.find(row => row.type === "session_meta")?.payload);
6
+ if (typeof metadata.id !== "string")
7
+ throw new Error("This is not a supported Codex rollout: session_meta.id is missing.");
8
+ const events = [];
9
+ let turnId = null;
10
+ for (const [ordinal, row] of rows.entries()) {
11
+ const payload = object(row.payload);
12
+ if (row.type === "session_meta")
13
+ continue;
14
+ if (row.type === "turn_context") {
15
+ turnId = typeof payload.turn_id === "string" ? payload.turn_id : turnId;
16
+ continue;
17
+ }
18
+ const id = typeof payload.id === "string" ? payload.id : `codex-event-${contentHash([ordinal, row]).slice(0, 32)}`;
19
+ const base = { id, occurredAt: connectedTimestamp(row.timestamp), parentId: turnId };
20
+ if (row.type === "response_item") {
21
+ if (payload.type === "message" && ["user", "assistant", "system", "developer"].includes(String(payload.role)))
22
+ events.push(event({ ...base, kind: "message", role: payload.role === "developer" ? "system" : payload.role, content: json(payload.content) }));
23
+ else if (payload.type === "function_call" || payload.type === "custom_tool_call")
24
+ events.push(event({ ...base, kind: "tool_call", callId: typeof payload.call_id === "string" ? payload.call_id : null, content: json(payload) }));
25
+ else if (payload.type === "function_call_output" || payload.type === "custom_tool_call_output")
26
+ events.push(event({ ...base, kind: "tool_result", role: "tool", callId: typeof payload.call_id === "string" ? payload.call_id : null, content: json(payload.output) }));
27
+ else
28
+ events.push(event({ ...base, kind: "unknown", content: json(payload) }));
29
+ }
30
+ else if (row.type === "compacted")
31
+ events.push(event({ ...base, kind: "compaction", content: json(payload) }));
32
+ else if (row.type === "event_msg") {
33
+ // These events mirror response_item messages. Keeping both doubles turns.
34
+ if (["user_message", "agent_message", "agent_reasoning", "task_started"].includes(String(payload.type)))
35
+ continue;
36
+ if (payload.type === "task_complete" || payload.type === "turn_aborted")
37
+ events.push(event({ ...base, kind: "terminal", content: { status: payload.type === "task_complete" ? "completed" : "cancelled", source: json(payload) } }));
38
+ else if (payload.type === "token_count") {
39
+ const info = object(payload.info);
40
+ const usage = sourceUsage(info.total_token_usage, `codex-cumulative:${metadata.id}:${ordinal}`, "reported_cumulative");
41
+ events.push(event({ ...base, kind: "usage", usage, content: json(payload) }));
42
+ }
43
+ else
44
+ events.push(event({ ...base, kind: "unknown", content: json(payload) }));
45
+ }
46
+ else
47
+ events.push(event({ ...base, kind: "unknown", content: json(row) }));
48
+ }
49
+ return normalizeConnectedSession({ origin: "codex", sessionId: metadata.id, parentSessionId: typeof metadata.forked_from_id === "string" ? metadata.forked_from_id : null,
50
+ exporterVersion: typeof metadata.cli_version === "string" ? metadata.cli_version : null, files: [file], events,
51
+ warnings: ["Codex cumulative usage is retained as reported totals and is not summed as billed invocations.", "Referenced files are unavailable unless separately admitted."] });
52
+ }
@@ -0,0 +1,86 @@
1
+ import { contentHash } from "@openpond/harness";
2
+ import { connectedJsonLines, connectedTimestamp, event, json, normalizeConnectedSession, object } from "../normalize.js";
3
+ import { sourceUsage } from "./usage.js";
4
+ function historyEvents(messages, origin) {
5
+ const events = [];
6
+ for (const [index, value] of messages.entries()) {
7
+ const message = object(value);
8
+ const id = typeof message.entryId === "string" ? message.entryId : typeof message.id === "string" ? message.id : typeof message.id === "number" && Number.isSafeInteger(message.id) ? String(message.id)
9
+ : typeof message.message_id === "string" ? message.message_id : `${origin}-${contentHash([index, message]).slice(0, 32)}`;
10
+ const base = { occurredAt: connectedTimestamp(message.timestamp, origin === "hermes" ? "seconds" : "milliseconds"),
11
+ parentId: typeof message.parentId === "string" ? message.parentId : null };
12
+ if (message.role === "tool") {
13
+ events.push(event({ ...base, id, kind: "tool_result", role: "tool", callId: typeof message.tool_call_id === "string" ? message.tool_call_id : null, content: json(message.content) }));
14
+ continue;
15
+ }
16
+ if (!["user", "assistant", "system"].includes(String(message.role))) {
17
+ events.push(event({ ...base, id, kind: "unknown", content: json(message) }));
18
+ continue;
19
+ }
20
+ events.push(event({ ...base, id, kind: "message", role: message.role, content: json(message.content),
21
+ usage: sourceUsage(message.usage, id) }));
22
+ if (Array.isArray(message.tool_calls))
23
+ for (const [ordinal, value] of message.tool_calls.entries()) {
24
+ const call = object(value);
25
+ events.push(event({ ...base, id: `${id}:call:${ordinal}`, kind: "tool_call", callId: typeof call.id === "string" ? call.id : null, content: json(call) }));
26
+ }
27
+ if (message.finish_reason === "stop" || message.stop_reason === "end_turn")
28
+ events.push(event({ ...base, id: `${id}:terminal`, kind: "terminal", content: { status: "completed" } }));
29
+ }
30
+ return events;
31
+ }
32
+ export function parseHermesEvidence(file, row) {
33
+ const sessionId = typeof row.id === "string" ? row.id : typeof row.session_id === "string" ? row.session_id : null;
34
+ if (!sessionId || !Array.isArray(row.messages))
35
+ throw new Error("Hermes requires a full-session export with id or session_id and messages.");
36
+ if (row.message_count !== undefined && row.message_count !== row.messages.length)
37
+ throw new Error("Hermes full-session message count differs from the retained messages.");
38
+ const lineage = Array.isArray(row.lineage_session_ids) ? row.lineage_session_ids.find(id => typeof id === "string" && id) : null;
39
+ const events = historyEvents(row.messages, "hermes");
40
+ if (typeof row.system_prompt === "string" && row.system_prompt.trim())
41
+ events.unshift(event({ id: `hermes-system-${contentHash(row.system_prompt).slice(0, 32)}`, kind: "message", role: "system", content: row.system_prompt }));
42
+ return normalizeConnectedSession({ origin: "hermes", sessionId, parentSessionId: typeof lineage === "string" ? lineage : typeof row.parent_session_id === "string" ? row.parent_session_id : null,
43
+ exporterVersion: typeof row.version === "string" ? row.version : null, files: [file], events,
44
+ warnings: ["Reported usage is not an OpenPond billing receipt.", "Exporter redaction and missing tools/artifacts remain evidence gaps.", ...(row.session_id && !row.id ? ["This is a Hermes session backup. Exporter version and terminal receipts may be unavailable."] : [])] });
45
+ }
46
+ export function parseOpenClawEvidence(files) {
47
+ const read = (path) => { const file = files.find(file => file.path === path); if (!file)
48
+ throw new Error(`OpenClaw bundle is missing ${path}.`); return file; };
49
+ const manifest = object(JSON.parse(read("manifest.json").text));
50
+ const branch = object(JSON.parse(read("session-branch.json").text));
51
+ const eventsFile = read("events.jsonl"), rows = connectedJsonLines(eventsFile);
52
+ if (manifest.traceSchema !== "openclaw-trajectory" || manifest.schemaVersion !== 1 || typeof manifest.sessionId !== "string" || typeof manifest.traceId !== "string")
53
+ throw new Error("Unsupported OpenClaw trajectory manifest.");
54
+ if (manifest.leafId !== branch.leafId || manifest.eventCount !== rows.length || rows.some(row => row.traceSchema !== manifest.traceSchema || row.schemaVersion !== 1 || row.sessionId !== manifest.sessionId || row.traceId !== manifest.traceId))
55
+ throw new Error("OpenClaw bundle identity/count does not match its manifest.");
56
+ if (!Array.isArray(branch.entries))
57
+ throw new Error("OpenClaw active branch entries are missing.");
58
+ if (branch.header && object(branch.header).id !== manifest.sessionId)
59
+ throw new Error("OpenClaw branch header differs from the manifest session.");
60
+ const ids = new Set(), messages = [];
61
+ for (const [index, value] of branch.entries.entries()) {
62
+ const entry = object(value);
63
+ if (typeof entry.id !== "string" || ids.has(entry.id) || (index > 0 && entry.parentId !== object(branch.entries[index - 1]).id))
64
+ throw new Error("OpenClaw active branch is duplicate, incomplete or unordered.");
65
+ ids.add(entry.id);
66
+ if (entry.type === "message")
67
+ messages.push({ ...object(entry.message), entryId: entry.id,
68
+ parentId: entry.parentId ?? null, timestamp: entry.timestamp ?? object(entry.message).timestamp });
69
+ }
70
+ if (branch.entries.length && object(branch.entries.at(-1)).id !== branch.leafId)
71
+ throw new Error("OpenClaw branch does not reach its leaf.");
72
+ // Branch messages carry context/output. Retain original events separately as
73
+ // unknown process evidence until their exact lifecycle mapping is qualified.
74
+ const events = historyEvents(messages, "openclaw"), unmappedEvents = [];
75
+ const seenSequences = new Set();
76
+ for (const [index, row] of rows.entries()) {
77
+ if (!Number.isSafeInteger(row.seq) || row.seq < 0 || seenSequences.has(row.seq) || index > 0 && Number(rows[index - 1].seq) >= Number(row.seq))
78
+ throw new Error("OpenClaw events have invalid/duplicate/unordered sequences.");
79
+ seenSequences.add(row.seq);
80
+ unmappedEvents.push(event({ id: `trajectory:${manifest.traceId}:${row.seq}`, kind: "unknown", occurredAt: connectedTimestamp(row.ts), content: json(row) }));
81
+ }
82
+ return normalizeConnectedSession({ origin: "openclaw", sessionId: manifest.sessionId, files, events, unmappedEvents,
83
+ exporterVersion: typeof manifest.openclawVersion === "string" ? manifest.openclawVersion : null,
84
+ warnings: ["Raw trajectory events are retained; unqualified event types are not advertised as complete process grading.",
85
+ ...(Array.isArray(manifest.warnings) ? manifest.warnings.map(value => typeof object(value).message === "string" ? String(object(value).message).slice(0, 500) : "The source exporter reports incomplete evidence.") : [])].slice(0, 100) });
86
+ }
@@ -0,0 +1,15 @@
1
+ import { object } from "../normalize.js";
2
+ export function sourceUsage(value, invocationId, provenance = "reported_invocation") {
3
+ const usage = object(value);
4
+ const number = (...values) => {
5
+ const value = values.find(item => typeof item === "number" && Number.isSafeInteger(item) && item >= 0);
6
+ return typeof value === "number" ? value : null;
7
+ };
8
+ const inputTokens = number(usage.input_tokens, usage.prompt_tokens);
9
+ const outputTokens = number(usage.output_tokens, usage.completion_tokens);
10
+ const cachedInputTokens = number(usage.cache_read_input_tokens, usage.cached_input_tokens, object(usage.input_tokens_details).cached_tokens);
11
+ const totalTokens = number(usage.total_tokens);
12
+ if (inputTokens === null && outputTokens === null && cachedInputTokens === null && totalTokens === null)
13
+ return null;
14
+ return { invocationId, inputTokens, outputTokens, cachedInputTokens, totalTokens, provenance };
15
+ }
@@ -0,0 +1,52 @@
1
+ import { z } from "zod";
2
+ import { contentHash } from "@openpond/harness";
3
+ const count = z.number().int().nonnegative().nullable();
4
+ export const ConnectedInvocationSchema = z.object({ requestId: z.string().min(1), startedAt: z.string().datetime(),
5
+ provider: z.string(), model: z.string(), mode: z.string(), status: z.string(), source: z.string(), projectId: z.string().nullable(),
6
+ inputTokens: count, cachedInputTokens: count, outputTokens: count, totalTokens: count }).strict();
7
+ const empty = () => ({ invocations: 0, inputTokens: 0, cachedInputTokens: 0, outputTokens: 0, totalTokens: 0,
8
+ missingInput: 0, missingCachedInput: 0, missingOutput: 0, missingTotal: 0, failures: 0, unattributed: 0 });
9
+ const nonnegative = z.number().int().nonnegative();
10
+ export const ConnectedUsageTotalsSchema = z.object({ invocations: nonnegative, inputTokens: nonnegative, cachedInputTokens: nonnegative,
11
+ outputTokens: nonnegative, totalTokens: nonnegative, missingInput: nonnegative, missingCachedInput: nonnegative,
12
+ missingOutput: nonnegative, missingTotal: nonnegative, failures: nonnegative, unattributed: nonnegative }).strict();
13
+ export const ConnectedUsageSummarySchema = z.object({ total: ConnectedUsageTotalsSchema,
14
+ days: z.array(ConnectedUsageTotalsSchema.extend({ key: z.string().regex(/^\d{4}-\d{2}-\d{2}$/) })),
15
+ modes: z.array(ConnectedUsageTotalsSchema.extend({ key: z.string() })), models: z.array(ConnectedUsageTotalsSchema.extend({ key: z.string() })) }).strict();
16
+ function add(total, row) {
17
+ total.invocations++;
18
+ total.failures += row.status === "completed" ? 0 : 1;
19
+ total.unattributed += row.projectId === null ? 1 : 0;
20
+ for (const [value, missing] of [["inputTokens", "missingInput"], ["cachedInputTokens", "missingCachedInput"], ["outputTokens", "missingOutput"], ["totalTokens", "missingTotal"]]) {
21
+ const measured = row[value];
22
+ if (measured === null)
23
+ total[missing]++;
24
+ else
25
+ total[value] += measured;
26
+ if (!Number.isSafeInteger(total[value]))
27
+ throw new Error("connected_usage_sum_overflow");
28
+ }
29
+ }
30
+ /** A billed retry has a new invocation identity; repeated copies of one receipt do not add spend.
31
+ * Unknown token fields remain unknown and are never inferred from another field.
32
+ */
33
+ export function summarizeConnectedInvocations(raw) {
34
+ const unique = new Map();
35
+ for (const value of raw) {
36
+ const row = ConnectedInvocationSchema.parse(value), prior = unique.get(row.requestId);
37
+ if (prior && contentHash(prior) !== contentHash(row))
38
+ throw new Error("connected_usage_receipt_conflict");
39
+ unique.set(row.requestId, row);
40
+ }
41
+ const total = empty(), days = new Map(), modes = new Map(), models = new Map();
42
+ for (const row of unique.values()) {
43
+ add(total, row);
44
+ for (const [map, key] of [[days, row.startedAt.slice(0, 10)], [modes, row.mode], [models, `${row.provider}/${row.model}`]]) {
45
+ const aggregate = map.get(key) ?? empty();
46
+ add(aggregate, row);
47
+ map.set(key, aggregate);
48
+ }
49
+ }
50
+ const entries = (map) => [...map].sort(([a], [b]) => a.localeCompare(b)).map(([key, totals]) => ({ key, ...totals }));
51
+ return { total, days: entries(days), modes: entries(modes), models: entries(models) };
52
+ }