@openpond/evals 0.11.18 → 0.11.19

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 (134) hide show
  1. package/dist/connected-evidence/contracts.js +14 -4
  2. package/dist/connected-evidence/imports.js +77 -17
  3. package/dist/connected-evidence/normalize.js +4 -3
  4. package/dist/connected-evidence/sources/claude-branches.js +52 -0
  5. package/dist/connected-evidence/sources/claude-code.js +7 -29
  6. package/dist/connected-evidence/sources/codex-authorization.js +40 -0
  7. package/dist/connected-evidence/sources/codex.js +7 -3
  8. package/dist/connected-evidence/sources/grok-build.js +137 -0
  9. package/dist/connected-evidence/sources/opencode.js +101 -0
  10. package/dist/connected-evidence/sources/pi.js +187 -0
  11. package/dist/graders.js +11 -1
  12. package/dist/learning/budgeted-reward-fixtures.js +1 -0
  13. package/dist/learning/contracts.js +10 -0
  14. package/dist/learning/index.js +1 -0
  15. package/dist/learning/model-judge.js +22 -5
  16. package/dist/learning/reward-calibration-closure.js +110 -0
  17. package/dist/learning/reward-calibration.js +20 -10
  18. package/dist/learning/reward-checks.js +1 -0
  19. package/dist/native-conversations/appimage-runtime.js +131 -0
  20. package/dist/native-conversations/collector-branches.js +79 -0
  21. package/dist/native-conversations/collector-contracts.js +9 -0
  22. package/dist/native-conversations/collector-destinations.js +27 -0
  23. package/dist/native-conversations/collector-errors.js +42 -0
  24. package/dist/native-conversations/collector-progress.js +114 -0
  25. package/dist/native-conversations/collector-store.js +247 -0
  26. package/dist/native-conversations/collector-sync-requests.js +52 -0
  27. package/dist/native-conversations/collector.js +322 -0
  28. package/dist/native-conversations/contracts.js +11 -0
  29. package/dist/native-conversations/database.js +149 -0
  30. package/dist/native-conversations/discovery.js +135 -0
  31. package/dist/native-conversations/history.js +298 -0
  32. package/dist/native-conversations/index.js +11 -0
  33. package/dist/native-conversations/location.js +29 -0
  34. package/dist/native-conversations/openclaw-cold-archive.js +91 -0
  35. package/dist/native-conversations/openclaw-database.js +130 -0
  36. package/dist/native-conversations/source-locations.js +112 -0
  37. package/dist/native-conversations/supervisor.js +208 -0
  38. package/dist/rewards.js +1 -1
  39. package/dist/types/connected-evidence/contracts.d.ts +15 -0
  40. package/dist/types/connected-evidence/contracts.d.ts.map +1 -1
  41. package/dist/types/connected-evidence/imports.d.ts +6 -0
  42. package/dist/types/connected-evidence/imports.d.ts.map +1 -1
  43. package/dist/types/connected-evidence/normalize.d.ts +4 -0
  44. package/dist/types/connected-evidence/normalize.d.ts.map +1 -1
  45. package/dist/types/connected-evidence/sources/claude-branches.d.ts +19 -0
  46. package/dist/types/connected-evidence/sources/claude-branches.d.ts.map +1 -0
  47. package/dist/types/connected-evidence/sources/claude-code.d.ts +5 -1
  48. package/dist/types/connected-evidence/sources/claude-code.d.ts.map +1 -1
  49. package/dist/types/connected-evidence/sources/codex-authorization.d.ts +18 -0
  50. package/dist/types/connected-evidence/sources/codex-authorization.d.ts.map +1 -0
  51. package/dist/types/connected-evidence/sources/codex.d.ts +5 -1
  52. package/dist/types/connected-evidence/sources/codex.d.ts.map +1 -1
  53. package/dist/types/connected-evidence/sources/grok-build.d.ts +79 -0
  54. package/dist/types/connected-evidence/sources/grok-build.d.ts.map +1 -0
  55. package/dist/types/connected-evidence/sources/history.d.ts +10 -2
  56. package/dist/types/connected-evidence/sources/history.d.ts.map +1 -1
  57. package/dist/types/connected-evidence/sources/opencode.d.ts +78 -0
  58. package/dist/types/connected-evidence/sources/opencode.d.ts.map +1 -0
  59. package/dist/types/connected-evidence/sources/pi.d.ts +79 -0
  60. package/dist/types/connected-evidence/sources/pi.d.ts.map +1 -0
  61. package/dist/types/graders.d.ts +81 -0
  62. package/dist/types/graders.d.ts.map +1 -1
  63. package/dist/types/learning/budgeted-reward-fixtures.d.ts +2 -1
  64. package/dist/types/learning/budgeted-reward-fixtures.d.ts.map +1 -1
  65. package/dist/types/learning/contracts.d.ts +167 -0
  66. package/dist/types/learning/contracts.d.ts.map +1 -1
  67. package/dist/types/learning/index.d.ts +1 -0
  68. package/dist/types/learning/index.d.ts.map +1 -1
  69. package/dist/types/learning/judge-budget.d.ts +3 -0
  70. package/dist/types/learning/judge-budget.d.ts.map +1 -1
  71. package/dist/types/learning/model-judge.d.ts +6 -1
  72. package/dist/types/learning/model-judge.d.ts.map +1 -1
  73. package/dist/types/learning/operations.d.ts +44 -0
  74. package/dist/types/learning/operations.d.ts.map +1 -1
  75. package/dist/types/learning/repository.d.ts +81 -0
  76. package/dist/types/learning/repository.d.ts.map +1 -1
  77. package/dist/types/learning/reward-calibration-closure.d.ts +2323 -0
  78. package/dist/types/learning/reward-calibration-closure.d.ts.map +1 -0
  79. package/dist/types/learning/reward-calibration.d.ts +1 -1
  80. package/dist/types/learning/reward-calibration.d.ts.map +1 -1
  81. package/dist/types/learning/reward-checks.d.ts +35 -0
  82. package/dist/types/learning/reward-checks.d.ts.map +1 -1
  83. package/dist/types/learning/transport.d.ts +103 -0
  84. package/dist/types/learning/transport.d.ts.map +1 -1
  85. package/dist/types/native-conversations/appimage-runtime.d.ts +12 -0
  86. package/dist/types/native-conversations/appimage-runtime.d.ts.map +1 -0
  87. package/dist/types/native-conversations/collector-branches.d.ts +22 -0
  88. package/dist/types/native-conversations/collector-branches.d.ts.map +1 -0
  89. package/dist/types/native-conversations/collector-contracts.d.ts +88 -0
  90. package/dist/types/native-conversations/collector-contracts.d.ts.map +1 -0
  91. package/dist/types/native-conversations/collector-destinations.d.ts +18 -0
  92. package/dist/types/native-conversations/collector-destinations.d.ts.map +1 -0
  93. package/dist/types/native-conversations/collector-errors.d.ts +13 -0
  94. package/dist/types/native-conversations/collector-errors.d.ts.map +1 -0
  95. package/dist/types/native-conversations/collector-progress.d.ts +24 -0
  96. package/dist/types/native-conversations/collector-progress.d.ts.map +1 -0
  97. package/dist/types/native-conversations/collector-store.d.ts +31 -0
  98. package/dist/types/native-conversations/collector-store.d.ts.map +1 -0
  99. package/dist/types/native-conversations/collector-sync-requests.d.ts +25 -0
  100. package/dist/types/native-conversations/collector-sync-requests.d.ts.map +1 -0
  101. package/dist/types/native-conversations/collector.d.ts +8 -0
  102. package/dist/types/native-conversations/collector.d.ts.map +1 -0
  103. package/dist/types/native-conversations/contracts.d.ts +47 -0
  104. package/dist/types/native-conversations/contracts.d.ts.map +1 -0
  105. package/dist/types/native-conversations/database.d.ts +14 -0
  106. package/dist/types/native-conversations/database.d.ts.map +1 -0
  107. package/dist/types/native-conversations/discovery.d.ts +9 -0
  108. package/dist/types/native-conversations/discovery.d.ts.map +1 -0
  109. package/dist/types/native-conversations/history.d.ts +23 -0
  110. package/dist/types/native-conversations/history.d.ts.map +1 -0
  111. package/dist/types/native-conversations/index.d.ts +12 -0
  112. package/dist/types/native-conversations/index.d.ts.map +1 -0
  113. package/dist/types/native-conversations/location.d.ts +3 -0
  114. package/dist/types/native-conversations/location.d.ts.map +1 -0
  115. package/dist/types/native-conversations/openclaw-cold-archive.d.ts +8 -0
  116. package/dist/types/native-conversations/openclaw-cold-archive.d.ts.map +1 -0
  117. package/dist/types/native-conversations/openclaw-database.d.ts +14 -0
  118. package/dist/types/native-conversations/openclaw-database.d.ts.map +1 -0
  119. package/dist/types/native-conversations/source-locations.d.ts +8 -0
  120. package/dist/types/native-conversations/source-locations.d.ts.map +1 -0
  121. package/dist/types/native-conversations/supervisor.d.ts +21 -0
  122. package/dist/types/native-conversations/supervisor.d.ts.map +1 -0
  123. package/dist/types/rewards.d.ts +50 -0
  124. package/dist/types/rewards.d.ts.map +1 -1
  125. package/dist/types/telemetry-analysis.d.ts +1 -1
  126. package/dist/types/telemetry-bundle.d.ts +2 -2
  127. package/dist/types/telemetry-catalog.d.ts +21 -21
  128. package/dist/types/telemetry.d.ts +2 -2
  129. package/package.json +11 -3
  130. package/schemas/learning/v1/command-request.schema.json +122 -91
  131. package/schemas/learning/v1/decision.schema.json +63 -0
  132. package/schemas/learning/v1/grade.schema.json +91 -0
  133. package/schemas/learning/v1/reward_check.schema.json +91 -0
  134. package/schemas/learning/v1/source.schema.json +28 -0
@@ -8,7 +8,7 @@ const Id = z.string().min(1).max(500);
8
8
  const Hash = z.string().regex(/^[a-f0-9]{64}$/);
9
9
  const Time = z.string().datetime().nullable();
10
10
  const Tokens = z.number().int().nonnegative().nullable();
11
- export const ConnectedSourceKindSchema = z.enum(["native_chat", "native_work", "codex", "claude_code", "hermes", "openclaw"]);
11
+ export const ConnectedSourceKindSchema = z.enum(["native_chat", "native_work", "codex", "claude_code", "hermes", "openclaw", "opencode", "grok_build", "pi", "oh_my_pi"]);
12
12
  export const ConnectedUsageSchema = z.object({
13
13
  invocationId: Id, inputTokens: Tokens, outputTokens: Tokens, cachedInputTokens: Tokens,
14
14
  totalTokens: Tokens, provenance: z.enum(["billed_invocation", "reported_invocation", "reported_cumulative"]),
@@ -33,6 +33,7 @@ export const ConnectedSessionSchema = z.object({
33
33
  schemaVersion: z.literal(CONNECTED_EVIDENCE_VERSION), normalizerVersion: z.literal(CONNECTED_NORMALIZER_VERSION),
34
34
  origin: ConnectedSourceKindSchema, sessionId: Id, branchId: Id.nullable(), parentSessionId: Id.nullable(),
35
35
  exporterVersion: z.string().max(200).nullable(),
36
+ acquisition: z.object({ machineId: Id, sourceInstanceId: Id }).strict().optional(),
36
37
  sourceFiles: z.array(z.object({ path: Id, contentHash: Hash, sizeBytes: z.number().int().nonnegative() }).strict()).max(CONNECTED_EVIDENCE_LIMITS.files),
37
38
  events: z.array(ConnectedEventSchema).max(CONNECTED_EVIDENCE_LIMITS.events),
38
39
  unmappedEvents: z.array(ConnectedEventSchema).max(CONNECTED_EVIDENCE_LIMITS.events),
@@ -48,8 +49,8 @@ export function hasRecordedConnectedAnswer(event) {
48
49
  return event.content.length > 0;
49
50
  return typeof event.content !== "object" || !("text" in event.content) || typeof event.content.text === "string" && event.content.text.trim().length > 0;
50
51
  }
51
- /** Bytes stay in one immutable snapshot; cases retain cutoffs, not copied history. */
52
- export function resolveConnectedBoundary(sessionValue, boundaryId) {
52
+ /** Shared integrity verification for metadata and evaluator evidence. */
53
+ function verifiedConnectedBoundary(sessionValue, boundaryId) {
53
54
  const session = ConnectedSessionSchema.parse(sessionValue);
54
55
  const { contentHash: actual, ...body } = session;
55
56
  if (contentHash(body) !== actual)
@@ -65,7 +66,16 @@ export function resolveConnectedBoundary(sessionValue, boundaryId) {
65
66
  const answer = boundary.projection === "conversation" ? answers.map(event => event.content) : answers.at(-1)?.content ?? null;
66
67
  if ((answers.length ? contentHash(answer) : null) !== boundary.outputHash)
67
68
  throw new Error("connected_output_hash_mismatch");
68
- const value = { input, observed, answer, boundary };
69
+ return { input, observed, answer, boundary };
70
+ }
71
+ /** Status/discovery returns no evaluator payload. Large retained histories still
72
+ * require the exact session, input, observed-output and answer hashes to match. */
73
+ export function resolveConnectedBoundaryMetadata(sessionValue, boundaryId) {
74
+ return verifiedConnectedBoundary(sessionValue, boundaryId).boundary;
75
+ }
76
+ /** Bytes stay in one immutable snapshot; cases retain cutoffs, not copied history. */
77
+ export function resolveConnectedBoundary(sessionValue, boundaryId) {
78
+ const value = verifiedConnectedBoundary(sessionValue, boundaryId);
69
79
  if (new TextEncoder().encode(JSON.stringify(value)).length > CONNECTED_EVIDENCE_LIMITS.evaluatorBytes)
70
80
  throw new Error("connected_evaluator_context_too_large");
71
81
  return value;
@@ -1,17 +1,32 @@
1
1
  import { contentHash } from "@openpond/harness";
2
- import { CONNECTED_EVIDENCE_LIMITS } from "./contracts.js";
3
- import { connectedJsonLines } from "./normalize.js";
2
+ import { CONNECTED_EVIDENCE_LIMITS, CONNECTED_NORMALIZER_VERSION, } from "./contracts.js";
3
+ import { connectedJsonLines, normalizeConnectedSession } from "./normalize.js";
4
+ import { parseGrokBuildSession } from "./sources/grok-build.js";
5
+ import { parsePiSession } from "./sources/pi.js";
6
+ import { parseOpenCodeSession } from "./sources/opencode.js";
7
+ import { CODEX_AUTHORIZATION_PROJECTION_POLICY } from "./sources/codex-authorization.js";
4
8
  import { parseCodexSession } from "./sources/codex.js";
5
9
  import { parseClaudeSession } from "./sources/claude-code.js";
6
- import { parseHermesEvidence, parseOpenClawEvidence } from "./sources/history.js";
10
+ import { parseHermesEvidence, parseOpenClawEvidence, } from "./sources/history.js";
11
+ /** Cache identity includes source projection policy, independent of file mtimes. */
12
+ export function agentImportNormalizerRevision(source) {
13
+ return source === "codex" ? `${CONNECTED_NORMALIZER_VERSION}/${CODEX_AUTHORIZATION_PROJECTION_POLICY}` : CONNECTED_NORMALIZER_VERSION;
14
+ }
7
15
  /** Never admits malformed files partly or executes referenced paths/commands. */
8
16
  export function previewAgentImport(input) {
9
- if (!input.files.length || input.files.length > CONNECTED_EVIDENCE_LIMITS.files)
17
+ if (!input.files.length ||
18
+ input.files.length > CONNECTED_EVIDENCE_LIMITS.files)
10
19
  throw new Error("Choose between 1 and 100 source files.");
11
20
  const paths = new Set();
12
21
  let bytes = 0;
13
22
  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))
23
+ if (file.path.length > 500 ||
24
+ file.path.startsWith("/") ||
25
+ file.path.includes("\\") ||
26
+ file.path
27
+ .split("/")
28
+ .some((part) => !part || part === "." || part === "..") ||
29
+ paths.has(file.path))
15
30
  throw new Error("Unsafe or duplicate source file path.");
16
31
  paths.add(file.path);
17
32
  bytes += new TextEncoder().encode(file.text).length;
@@ -21,22 +36,47 @@ export function previewAgentImport(input) {
21
36
  const sessions = [], issues = [];
22
37
  const attempt = (file, parse) => {
23
38
  try {
24
- sessions.push(...parse());
39
+ sessions.push(...parse().map((session) => input.acquisition
40
+ ? normalizeConnectedSession({
41
+ origin: session.origin,
42
+ sessionId: session.sessionId,
43
+ branchId: session.branchId,
44
+ parentSessionId: session.parentSessionId,
45
+ exporterVersion: session.exporterVersion,
46
+ files: input.files.filter((item) => session.sourceFiles.some((file) => file.path === item.path)),
47
+ events: session.events,
48
+ unmappedEvents: session.unmappedEvents,
49
+ warnings: session.warnings,
50
+ contextComplete: !session.boundaries.some((boundary) => boundary.coverage.context === "unknown"),
51
+ acquisition: input.acquisition,
52
+ })
53
+ : session));
25
54
  }
26
55
  catch (error) {
27
- issues.push({ file, message: error instanceof Error ? error.message : "Unsupported session." });
56
+ issues.push({
57
+ file,
58
+ message: error instanceof Error ? error.message : "Unsupported session.",
59
+ });
28
60
  }
29
61
  };
30
- if (input.source === "openclaw")
62
+ if (input.source === "openclaw" &&
63
+ input.files.some((file) => file.path.endsWith("manifest.json")))
31
64
  attempt("manifest.json", () => {
32
65
  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;
66
+ const files = !input.files.some((file) => file.path === "manifest.json") &&
67
+ input.files.every((file) => file.path.startsWith(`${root}/`))
68
+ ? input.files.map((file) => ({
69
+ ...file,
70
+ path: file.path.slice(root.length + 1),
71
+ }))
72
+ : input.files;
35
73
  return [parseOpenClawEvidence(files)];
36
74
  });
37
75
  else
38
76
  for (const file of input.files)
39
77
  attempt(file.path, () => {
78
+ if (input.source === "opencode")
79
+ return [parseOpenCodeSession(file, JSON.parse(file.text))];
40
80
  let rows;
41
81
  if (input.source === "hermes" && file.path.endsWith(".json")) {
42
82
  const value = JSON.parse(file.text.replace(/^\uFEFF/u, ""));
@@ -46,26 +86,46 @@ export function previewAgentImport(input) {
46
86
  }
47
87
  else
48
88
  rows = connectedJsonLines(file);
89
+ if (input.source === "grok_build")
90
+ return [parseGrokBuildSession(file, rows)];
91
+ if (input.source === "pi" ||
92
+ input.source === "oh_my_pi" ||
93
+ input.source === "openclaw")
94
+ return [parsePiSession(file, rows, input.source, input.branchLeafId)];
49
95
  if (input.source === "codex")
50
96
  return [parseCodexSession(file, rows)];
51
97
  if (input.source === "claude_code")
52
98
  return [parseClaudeSession(file, rows, input.branchLeafId)];
53
99
  if (input.source === "hermes")
54
- return rows.map(row => parseHermesEvidence(file, row));
100
+ return rows.map((row) => parseHermesEvidence(file, row));
55
101
  throw new Error("Unsupported source adapter.");
56
102
  });
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)
103
+ if (sessions.length > CONNECTED_EVIDENCE_LIMITS.sessions ||
104
+ sessions.reduce((total, session) => total + session.events.length + session.unmappedEvents.length, 0) > CONNECTED_EVIDENCE_LIMITS.events ||
105
+ sessions.reduce((total, session) => total + session.boundaries.length, 0) >
106
+ CONNECTED_EVIDENCE_LIMITS.boundaries)
59
107
  throw new Error("Import exceeds the session/event/boundary limit.");
60
108
  const identities = new Map();
61
109
  for (const session of sessions) {
62
- const key = JSON.stringify([session.origin, session.sessionId, session.branchId]);
110
+ const key = JSON.stringify([
111
+ session.origin,
112
+ session.sessionId,
113
+ session.branchId,
114
+ ]);
63
115
  const prior = identities.get(key);
64
116
  if (prior && prior !== session.contentHash)
65
117
  throw new Error("The same session has different contents in this selection. Choose one export.");
66
118
  identities.set(key, session.contentHash);
67
119
  }
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 };
120
+ return {
121
+ schemaVersion: "openpond.agentImportPreview.v1",
122
+ source: input.source,
123
+ sourceHash: contentHash({
124
+ source: input.source,
125
+ files: [...input.files].sort((a, b) => a.path.localeCompare(b.path)),
126
+ branchLeafId: input.branchLeafId ?? null,
127
+ }),
128
+ sessions: sessions.filter((session, index) => sessions.findIndex((other) => other.contentHash === session.contentHash) === index),
129
+ issues,
130
+ };
71
131
  }
@@ -20,7 +20,7 @@ export function normalizeConnectedSession(input) {
20
20
  throw new Error("The session has no retained user request.");
21
21
  if (starts.length + (input.includeConversation === false ? 0 : 1) > CONNECTED_EVIDENCE_LIMITS.boundaries)
22
22
  throw new Error("connected_boundary_limit");
23
- const familyKey = `family-${contentHash([input.origin, input.parentSessionId ?? input.sessionId]).slice(0, 32)}`;
23
+ const familyKey = `family-${contentHash([input.origin, ...(input.acquisition ? [input.acquisition.sourceInstanceId] : []), input.parentSessionId ?? input.sessionId]).slice(0, 32)}`;
24
24
  const boundaries = starts.map((start, position) => {
25
25
  const end = starts[position + 1] ?? events.length;
26
26
  const observed = events.slice(start + 1, end);
@@ -35,7 +35,7 @@ export function normalizeConnectedSession(input) {
35
35
  const compacted = events.slice(0, start + 1).some(event => event.kind === "compaction");
36
36
  const unknown = observed.some(event => event.kind === "unknown");
37
37
  return {
38
- id: `case-${contentHash([input.origin, input.sessionId, input.branchId ?? null, events[start].id]).slice(0, 40)}`,
38
+ id: `case-${contentHash([input.origin, ...(input.acquisition ? [input.acquisition.sourceInstanceId] : []), input.sessionId, input.branchId ?? null, events[start].id]).slice(0, 40)}`,
39
39
  familyKey, projection: "turn", requestEventId: events[start].id, start, end,
40
40
  inputHash: contentHash(events.slice(0, start + 1)), outputHash: answer ? contentHash(answer.content) : null,
41
41
  revisionHash: contentHash(observed), terminal: status === "completed" || status === "failed" || status === "cancelled" ? status : "unknown",
@@ -50,7 +50,7 @@ export function normalizeConnectedSession(input) {
50
50
  if (input.includeConversation !== false) {
51
51
  const first = boundaries[0], last = boundaries.at(-1);
52
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)}`,
53
+ boundaries.push({ ...first, id: `conversation-${contentHash([input.origin, ...(input.acquisition ? [input.acquisition.sourceInstanceId] : []), input.sessionId, input.branchId ?? null]).slice(0, 40)}`,
54
54
  projection: "conversation", end: events.length, outputHash: answers.length ? contentHash(answers.map(event => event.content)) : null,
55
55
  revisionHash: contentHash(observed), terminal: last.terminal, coverage: { ...first.coverage,
56
56
  answer: answers.length > 0, process: boundaries.some(item => item.coverage.process === "partial") ? "partial" : boundaries.some(item => item.coverage.process === "retained") ? "retained" : "absent",
@@ -61,6 +61,7 @@ export function normalizeConnectedSession(input) {
61
61
  const body = { schemaVersion: CONNECTED_EVIDENCE_VERSION, normalizerVersion: CONNECTED_NORMALIZER_VERSION,
62
62
  origin: input.origin, sessionId: input.sessionId, branchId: input.branchId ?? null, parentSessionId: input.parentSessionId ?? null,
63
63
  exporterVersion: input.exporterVersion ?? null, sourceFiles: input.files.map(connectedFileIdentity).sort((a, b) => a.path.localeCompare(b.path)),
64
+ ...(input.acquisition ? { acquisition: input.acquisition } : {}),
64
65
  events, unmappedEvents: (input.unmappedEvents ?? []).map((event, sequence) => ({ ...event, sequence })), boundaries, warnings: input.warnings ?? [] };
65
66
  const session = ConnectedSessionSchema.parse({ ...body, contentHash: contentHash(body) });
66
67
  if (new TextEncoder().encode(JSON.stringify(session)).length > CONNECTED_EVIDENCE_LIMITS.decodedBytes)
@@ -0,0 +1,52 @@
1
+ import { contentHash } from "@openpond/harness";
2
+ import { connectedTimestamp, object } from "../normalize.js";
3
+ /** One native graph owns selection, import and Desktop inspection semantics. */
4
+ export function claudeGraph(rows) {
5
+ const messages = rows.filter(row => (row.type === "user" || row.type === "assistant") && typeof row.uuid === "string");
6
+ const sessions = new Set(messages.flatMap(row => typeof row.sessionId === "string" ? [row.sessionId] : []));
7
+ if (sessions.size !== 1 || !messages.length)
8
+ throw new Error("A supported Claude Code transcript must identify one session with UUID messages.");
9
+ const sessionId = [...sessions][0];
10
+ const nodes = rows.filter(row => typeof row.uuid === "string" && row.sessionId === sessionId);
11
+ const byId = new Map(nodes.map(row => [row.uuid, row]));
12
+ if (byId.size !== nodes.length)
13
+ throw new Error("Claude Code transcript contains duplicate message UUIDs.");
14
+ const parents = new Set(nodes.flatMap(row => typeof row.parentUuid === "string" ? [row.parentUuid] : []));
15
+ const leaves = nodes.filter(row => !parents.has(row.uuid));
16
+ if (!leaves.length)
17
+ throw new Error("Claude Code transcript has no selectable branch leaf.");
18
+ function select(leafId) {
19
+ let current = byId.get(leafId);
20
+ if (!current)
21
+ throw new Error("The selected Claude Code branch leaf is missing.");
22
+ const chain = [], seen = new Set();
23
+ let missingParent = false;
24
+ while (current) {
25
+ const id = current.uuid;
26
+ if (seen.has(id))
27
+ throw new Error("Claude Code branch contains a parent cycle.");
28
+ seen.add(id);
29
+ chain.unshift(current);
30
+ const parent = current.parentUuid;
31
+ if (typeof parent !== "string")
32
+ break;
33
+ current = byId.get(parent);
34
+ if (!current)
35
+ missingParent = true;
36
+ }
37
+ return { chain, missingParent, chainHash: contentHash(chain) };
38
+ }
39
+ function branches() {
40
+ if (leaves.length > 256)
41
+ throw new Error("This transcript exceeds the 256-branch inspection limit.");
42
+ return leaves.map(leaf => {
43
+ const selected = select(leaf.uuid);
44
+ const messages = selected.chain.filter(row => row.type === "user" || row.type === "assistant");
45
+ const last = messages.at(-1);
46
+ const content = object(last?.message).content;
47
+ const title = typeof content === "string" ? content : Array.isArray(content) ? content.flatMap(block => typeof object(block).text === "string" ? [object(block).text] : []).join(" ") : "";
48
+ return { leafId: leaf.uuid, chainHash: selected.chainHash, title: title.slice(0, 180), updatedAt: connectedTimestamp(last?.timestamp), messages: messages.length };
49
+ });
50
+ }
51
+ return { sessionId, leaves, select, branches };
52
+ }
@@ -1,38 +1,16 @@
1
+ import { claudeGraph } from "./claude-branches.js";
1
2
  import { contentHash } from "@openpond/harness";
2
3
  import { connectedTimestamp, event, json, normalizeConnectedSession, object } from "../normalize.js";
3
4
  import { sourceUsage } from "./usage.js";
4
5
  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)
6
+ const graph = claudeGraph(rows);
7
+ if (!leafId && graph.leaves.length !== 1)
15
8
  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
- }
9
+ const { chain, missingParent } = graph.select(leafId ?? graph.leaves[0].uuid);
34
10
  const events = [];
35
11
  for (const row of chain) {
12
+ if (row.type !== "user" && row.type !== "assistant")
13
+ continue;
36
14
  const message = object(row.message), content = message.content;
37
15
  const base = { occurredAt: connectedTimestamp(row.timestamp), parentId: typeof row.parentUuid === "string" ? row.parentUuid : null };
38
16
  const role = row.type === "user" ? "user" : "assistant";
@@ -61,7 +39,7 @@ export function parseClaudeSession(file, rows, leafId) {
61
39
  }
62
40
  if (rows.some(row => row.type === "system" && row.subtype === "compact_boundary"))
63
41
  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],
42
+ return normalizeConnectedSession({ origin: "claude_code", sessionId: graph.sessionId,
65
43
  // A chosen leaf is a cutoff, not a new family or the identity of every turn.
66
44
  branchId: null, parentSessionId: typeof chain[0]?.parentSessionId === "string" ? chain[0].parentSessionId : null,
67
45
  exporterVersion: typeof chain.at(-1)?.version === "string" ? chain.at(-1).version : null,
@@ -0,0 +1,40 @@
1
+ import { contentHash } from "@openpond/harness";
2
+ import { object } from "../normalize.js";
3
+ export const CODEX_AUTHORIZATION_PROJECTION_POLICY = "codex-native-authorization-v1";
4
+ const POLICY = CODEX_AUTHORIZATION_PROJECTION_POLICY;
5
+ /** Native approved-command rules are account authority, not observed tool calls.
6
+ * Retain all other context and the original file/event identity. Unknown formats
7
+ * remain untouched for ordinary privacy admission; this is not secret scrubbing.
8
+ */
9
+ export function projectCodexAuthorization(row) {
10
+ const omissions = [];
11
+ const omit = (sourcePath, value, entries) => {
12
+ const receipt = { policy: POLICY, sourcePath, sourceHash: contentHash(value), entries };
13
+ omissions.push(receipt);
14
+ return receipt;
15
+ };
16
+ const permissionsText = (text, path) => text.replace(/<permissions instructions>[\s\S]*?<\/permissions instructions>/gu, block => block.replace(/^## Approved command prefixes\r?\n[\s\S]*?(?=^## |<\/permissions instructions>)/gmu, section => {
17
+ const receipt = omit(path, section, null);
18
+ return `## Approved command prefixes\n[Native authorization rules omitted; policy ${receipt.policy}; source hash ${receipt.sourceHash}]\n`;
19
+ }));
20
+ const payload = object(row.payload);
21
+ if (row.type === "response_item" && payload.type === "message" && ["system", "developer"].includes(String(payload.role))) {
22
+ const content = typeof payload.content === "string"
23
+ ? permissionsText(payload.content, "/payload/content")
24
+ : Array.isArray(payload.content) ? payload.content.map((value, index) => {
25
+ const block = object(value);
26
+ return typeof block.text === "string"
27
+ ? { ...block, text: permissionsText(block.text, `/payload/content/${index}/text`) }
28
+ : value;
29
+ }) : payload.content;
30
+ return { row: { ...row, payload: { ...payload, content } }, omissions };
31
+ }
32
+ if (row.type === "world_state") {
33
+ const state = object(payload.state), permissions = object(state.permissions);
34
+ if (Array.isArray(permissions.approved_command_prefixes)) {
35
+ const receipt = omit("/payload/state/permissions/approved_command_prefixes", permissions.approved_command_prefixes, permissions.approved_command_prefixes.length);
36
+ return { row: { ...row, payload: { ...payload, state: { ...state, permissions: { ...permissions, approved_command_prefixes: { omitted: "native_authorization_state", ...receipt } } } } }, omissions };
37
+ }
38
+ }
39
+ return { row, omissions };
40
+ }
@@ -1,5 +1,6 @@
1
1
  import { contentHash } from "@openpond/harness";
2
2
  import { connectedTimestamp, event, json, normalizeConnectedSession, object } from "../normalize.js";
3
+ import { projectCodexAuthorization } from "./codex-authorization.js";
3
4
  import { sourceUsage } from "./usage.js";
4
5
  export function parseCodexSession(file, rows) {
5
6
  const metadata = object(rows.find(row => row.type === "session_meta")?.payload);
@@ -7,8 +8,11 @@ export function parseCodexSession(file, rows) {
7
8
  throw new Error("This is not a supported Codex rollout: session_meta.id is missing.");
8
9
  const events = [];
9
10
  let turnId = null;
11
+ let authorizationOmissions = 0;
10
12
  for (const [ordinal, row] of rows.entries()) {
11
- const payload = object(row.payload);
13
+ const projected = projectCodexAuthorization(row);
14
+ authorizationOmissions += projected.omissions.length;
15
+ const payload = object(projected.row.payload);
12
16
  if (row.type === "session_meta")
13
17
  continue;
14
18
  if (row.type === "turn_context") {
@@ -44,9 +48,9 @@ export function parseCodexSession(file, rows) {
44
48
  events.push(event({ ...base, kind: "unknown", content: json(payload) }));
45
49
  }
46
50
  else
47
- events.push(event({ ...base, kind: "unknown", content: json(row) }));
51
+ events.push(event({ ...base, kind: "unknown", content: json(projected.row) }));
48
52
  }
49
53
  return normalizeConnectedSession({ origin: "codex", sessionId: metadata.id, parentSessionId: typeof metadata.forked_from_id === "string" ? metadata.forked_from_id : null,
50
54
  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."] });
55
+ warnings: ["Codex cumulative usage is retained as reported totals and is not summed as billed invocations.", "Referenced files are unavailable unless separately admitted.", ...(authorizationOmissions ? [`Codex native authorization state excluded by codex-native-authorization-v1 from ${authorizationOmissions} locations. Omission markers retain source-section hashes; sourceFiles retains original byte hashes. Other conversation evidence is unchanged and still requires privacy admission.`] : [])] });
52
56
  }
@@ -0,0 +1,137 @@
1
+ import { connectedTimestamp, event, json, normalizeConnectedSession, object, } from "../normalize.js";
2
+ import { sourceUsage } from "./usage.js";
3
+ /** Retained ACP updates are native Grok history; Markdown exports are not accepted. */
4
+ export function parseGrokBuildSession(file, rows) {
5
+ const sessions = new Set(rows
6
+ .map((row) => object(row.params).sessionId)
7
+ .filter((value) => typeof value === "string"));
8
+ if (sessions.size !== 1)
9
+ throw new Error("Grok Build updates must identify exactly one retained session.");
10
+ const events = [], ids = new Set(), calls = new Map();
11
+ let user = null, answer = null, completed = true;
12
+ for (const row of rows) {
13
+ const params = object(row.params), update = object(params.update), metadata = object(params._meta), kind = update.sessionUpdate;
14
+ if (typeof metadata.eventId !== "string" || ids.has(metadata.eventId))
15
+ throw new Error("Grok Build updates require unique native event IDs.");
16
+ ids.add(metadata.eventId);
17
+ const id = metadata.eventId, time = connectedTimestamp(metadata.agentTimestampMs ??
18
+ (typeof row.timestamp === "number" ? row.timestamp * 1000 : null));
19
+ const content = object(update.content);
20
+ if (kind === "user_message_chunk") {
21
+ if (content.type !== "text" || typeof content.text !== "string")
22
+ throw new Error("Grok user content type requires a qualified adapter.");
23
+ if (!user || completed) {
24
+ user = event({
25
+ id,
26
+ kind: "message",
27
+ role: "user",
28
+ occurredAt: time,
29
+ content: content.text,
30
+ });
31
+ events.push(user);
32
+ answer = null;
33
+ completed = false;
34
+ }
35
+ else
36
+ user.content = String(user.content) + content.text;
37
+ }
38
+ else if (kind === "agent_message_chunk") {
39
+ if (content.type !== "text" || typeof content.text !== "string") {
40
+ events.push(event({
41
+ id,
42
+ kind: "unknown",
43
+ occurredAt: time,
44
+ content: json(update),
45
+ }));
46
+ continue;
47
+ }
48
+ if (!answer) {
49
+ answer = event({
50
+ id,
51
+ kind: "message",
52
+ role: "assistant",
53
+ occurredAt: time,
54
+ content: content.text,
55
+ });
56
+ events.push(answer);
57
+ }
58
+ else
59
+ answer.content = String(answer.content) + content.text;
60
+ }
61
+ else if (kind === "tool_call" || kind === "tool_call_update") {
62
+ const callId = typeof update.toolCallId === "string" ? update.toolCallId : null;
63
+ if (!callId) {
64
+ events.push(event({
65
+ id,
66
+ kind: "unknown",
67
+ occurredAt: time,
68
+ content: json(update),
69
+ }));
70
+ continue;
71
+ }
72
+ let call = calls.get(callId);
73
+ if (!call) {
74
+ call = event({
75
+ id: `${id}:call`,
76
+ kind: "tool_call",
77
+ callId,
78
+ occurredAt: time,
79
+ content: json({ title: update.title, input: update.rawInput }),
80
+ });
81
+ calls.set(callId, call);
82
+ events.push(call);
83
+ }
84
+ else if (update.rawInput !== undefined)
85
+ call.content = json({ title: update.title, input: update.rawInput });
86
+ if (update.status === "completed" || update.status === "failed")
87
+ events.push(event({
88
+ id: `${id}:result`,
89
+ kind: "tool_result",
90
+ role: "tool",
91
+ callId,
92
+ occurredAt: time,
93
+ content: json({
94
+ status: update.status,
95
+ output: update.rawOutput,
96
+ content: update.content,
97
+ }),
98
+ }));
99
+ }
100
+ else if (kind === "turn_completed") {
101
+ const usage = object(update.usage), invocationId = typeof update.prompt_id === "string" ? update.prompt_id : id;
102
+ events.push(event({
103
+ id,
104
+ kind: "terminal",
105
+ occurredAt: time,
106
+ content: {
107
+ status: update.stop_reason === "end_turn"
108
+ ? "completed"
109
+ : update.stop_reason === "cancelled"
110
+ ? "cancelled"
111
+ : "failed",
112
+ },
113
+ usage: sourceUsage({
114
+ input_tokens: usage.inputTokens,
115
+ output_tokens: usage.outputTokens,
116
+ cached_input_tokens: usage.cachedReadTokens,
117
+ total_tokens: usage.totalTokens,
118
+ }, invocationId),
119
+ }));
120
+ completed = true;
121
+ answer = null;
122
+ user = null;
123
+ }
124
+ else if (kind !== "agent_thought_chunk")
125
+ events.push(event({ id, kind: "unknown", occurredAt: time, content: json(update) }));
126
+ }
127
+ return normalizeConnectedSession({
128
+ origin: "grok_build",
129
+ sessionId: String([...sessions][0]),
130
+ files: [file],
131
+ events,
132
+ contextComplete: false,
133
+ warnings: [
134
+ "Grok retained ACP updates omit system context and encrypted reasoning. Tool coverage reflects only logged updates.",
135
+ ],
136
+ });
137
+ }
@@ -0,0 +1,101 @@
1
+ import { connectedTimestamp, event, json, normalizeConnectedSession, object, } from "../normalize.js";
2
+ import { sourceUsage } from "./usage.js";
3
+ export function parseOpenCodeSession(file, value) {
4
+ const root = object(value), info = object(root.info);
5
+ if (typeof info.id !== "string" || !Array.isArray(root.messages))
6
+ throw new Error("OpenCode requires its native JSON session export (info and messages).");
7
+ const events = [];
8
+ for (const raw of root.messages) {
9
+ const item = object(raw), message = object(item.info), time = object(message.time);
10
+ if (typeof message.id !== "string" ||
11
+ message.sessionID !== info.id ||
12
+ !Array.isArray(item.parts))
13
+ throw new Error("OpenCode message identity does not match its session.");
14
+ const role = message.role;
15
+ if (role !== "user" && role !== "assistant")
16
+ throw new Error("Unsupported OpenCode message role.");
17
+ const base = {
18
+ occurredAt: connectedTimestamp(time.created),
19
+ parentId: typeof message.parentID === "string" ? message.parentID : null,
20
+ };
21
+ const parts = item.parts.map(object);
22
+ if (parts.some((part) => part.sessionID !== info.id ||
23
+ part.messageID !== message.id ||
24
+ typeof part.id !== "string"))
25
+ throw new Error("OpenCode part identity does not match its message.");
26
+ const text = parts
27
+ .filter((part) => part.type === "text")
28
+ .map((part) => String(part.text ?? ""))
29
+ .join("\n");
30
+ const tokens = object(message.tokens), cache = object(tokens.cache);
31
+ const usage = sourceUsage({
32
+ input_tokens: tokens.input,
33
+ output_tokens: tokens.output,
34
+ cached_input_tokens: cache.read,
35
+ total_tokens: tokens.total,
36
+ }, message.id);
37
+ if (text || role === "user")
38
+ events.push(event({
39
+ ...base,
40
+ id: message.id,
41
+ kind: "message",
42
+ role,
43
+ content: text,
44
+ usage,
45
+ }));
46
+ else if (usage)
47
+ events.push(event({
48
+ ...base,
49
+ id: `${message.id}:usage`,
50
+ kind: "usage",
51
+ content: null,
52
+ usage,
53
+ }));
54
+ for (const part of parts) {
55
+ const id = String(part.id);
56
+ if (part.type === "tool") {
57
+ const state = object(part.state), callId = typeof part.callID === "string" ? part.callID : null;
58
+ events.push(event({
59
+ ...base,
60
+ id: `${id}:call`,
61
+ kind: "tool_call",
62
+ callId,
63
+ content: json({ name: part.tool, arguments: state.input }),
64
+ }));
65
+ if (state.status === "completed" || state.status === "error")
66
+ events.push(event({
67
+ ...base,
68
+ id: `${id}:result`,
69
+ kind: "tool_result",
70
+ role: "tool",
71
+ callId,
72
+ content: json({
73
+ output: state.output,
74
+ error: state.error,
75
+ status: state.status,
76
+ }),
77
+ }));
78
+ }
79
+ else if (part.type === "compaction")
80
+ events.push(event({ ...base, id, kind: "compaction", content: json(part) }));
81
+ else if (!["text", "reasoning", "step-start", "step-finish"].includes(String(part.type)))
82
+ events.push(event({ ...base, id, kind: "unknown", content: json(part) }));
83
+ }
84
+ if (role === "assistant" && (message.finish === "stop" || message.error))
85
+ events.push(event({
86
+ ...base,
87
+ id: `${message.id}:terminal`,
88
+ kind: "terminal",
89
+ content: { status: message.error ? "failed" : "completed" },
90
+ }));
91
+ }
92
+ return normalizeConnectedSession({
93
+ origin: "opencode",
94
+ sessionId: info.id,
95
+ parentSessionId: typeof info.parentID === "string" ? info.parentID : null,
96
+ exporterVersion: typeof info.version === "string" ? info.version : null,
97
+ files: [file],
98
+ events,
99
+ warnings: ["Source-reported usage is not an OpenPond billing receipt."],
100
+ });
101
+ }