@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
@@ -0,0 +1,298 @@
1
+ import { open, lstat, readdir, readFile, realpath, stat, } from "node:fs/promises";
2
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
+ import { zstdDecompressSync } from "node:zlib";
4
+ import { previewAgentImport } from "../connected-evidence/imports.js";
5
+ import { connectedJsonLines, object } from "../connected-evidence/normalize.js";
6
+ import { claudeGraph } from "../connected-evidence/sources/claude-branches.js";
7
+ import { contentHash } from "@openpond/harness";
8
+ import { listDatabaseSessions, readDatabaseSession } from "./database.js";
9
+ import { NATIVE_READ_LIMIT, } from "./contracts.js";
10
+ async function selectedPath(source, path, boundSize = true) {
11
+ const root = await realpath(source.root), target = await realpath(resolve(root, path));
12
+ const child = relative(root, target);
13
+ if (child.startsWith(`..${sep}`) || child === ".." || isAbsolute(child))
14
+ throw new Error("History path is outside the selected source.");
15
+ if (boundSize && (await stat(target)).size > NATIVE_READ_LIMIT)
16
+ throw new Error("Source file exceeds 32 MiB.");
17
+ return target;
18
+ }
19
+ async function readSelectedFile(source, path) {
20
+ const target = await selectedPath(source, path), bytes = await readFile(target);
21
+ if (bytes.byteLength > NATIVE_READ_LIMIT)
22
+ throw new Error("Source changed beyond the retained limit.");
23
+ const decoded = target.endsWith(".zst")
24
+ ? zstdDecompressSync(bytes, { maxOutputLength: 64 * 1024 * 1024 })
25
+ : bytes;
26
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(decoded);
27
+ // A writer may be appending its last line. Only complete JSONL records enter snapshots.
28
+ if (target.endsWith(".jsonl") && text && !text.endsWith("\n")) {
29
+ const last = text.lastIndexOf("\n");
30
+ try {
31
+ JSON.parse(text.slice(last + 1));
32
+ }
33
+ catch {
34
+ return text.slice(0, last + 1);
35
+ }
36
+ }
37
+ return text;
38
+ }
39
+ async function pathsBelow(source) {
40
+ if ((await lstat(source.root)).isFile())
41
+ return [""];
42
+ const roots = source.source === "codex"
43
+ ? ["sessions", "archived_sessions"]
44
+ : source.source === "grok_build"
45
+ ? ["sessions"]
46
+ : source.source === "claude_code"
47
+ ? ["projects"]
48
+ : source.source === "pi" || source.source === "oh_my_pi"
49
+ ? ["sessions"]
50
+ : [""];
51
+ const paths = [];
52
+ async function walk(directory, depth) {
53
+ if (depth > 12)
54
+ throw new Error("Selected history exceeds directory depth limit.");
55
+ let entries;
56
+ try {
57
+ entries = await readdir(join(source.root, directory), {
58
+ withFileTypes: true,
59
+ });
60
+ }
61
+ catch (error) {
62
+ if (error.code === "ENOENT")
63
+ return;
64
+ throw error;
65
+ }
66
+ for (const entry of entries) {
67
+ const path = join(directory, entry.name);
68
+ if (entry.isSymbolicLink())
69
+ continue;
70
+ if (entry.isDirectory())
71
+ await walk(path, depth + 1);
72
+ else if (entry.isFile() &&
73
+ (source.source === "openclaw"
74
+ ? entry.name === "manifest.json"
75
+ : source.source === "grok_build"
76
+ ? entry.name === "updates.jsonl"
77
+ : /\.jsonl(?:\.zst)?$/u.test(entry.name)))
78
+ paths.push(path);
79
+ if (paths.length > 10000)
80
+ throw new Error("Select a narrower source directory (more than 10,000 history files).");
81
+ }
82
+ }
83
+ for (const root of roots)
84
+ await walk(root, 0);
85
+ // Explicit export directories need not mimic the application's home layout.
86
+ if (!paths.length && roots[0] !== "")
87
+ await walk("", 0);
88
+ return paths.sort();
89
+ }
90
+ function sessionFileJson(source, path) {
91
+ return (path || source.root).endsWith(".json");
92
+ }
93
+ export async function listSessions(source, input = {}) {
94
+ if (!source.available || !source.capabilities.history)
95
+ throw new Error(source.reason ?? "Native history is unavailable.");
96
+ const limit = input.limit ?? 50;
97
+ if (!Number.isInteger(limit) ||
98
+ limit < 1 ||
99
+ limit > 100 ||
100
+ (input.since && !Number.isFinite(Date.parse(input.since))))
101
+ throw new Error("Invalid history page or date range.");
102
+ if (source.acquisition === "sqlite")
103
+ return listDatabaseSessions(source, { ...input, limit });
104
+ const paths = await pathsBelow(source), candidates = [];
105
+ for (const path of paths) {
106
+ if (input.cursor && path <= input.cursor)
107
+ continue;
108
+ try {
109
+ const modified = await stat(join(source.root, path));
110
+ if (input.since && modified.mtimeMs < Date.parse(input.since))
111
+ continue;
112
+ let text;
113
+ if (source.acquisition !== "bundle" &&
114
+ !sessionFileJson(source, path) &&
115
+ source.source !== "hermes" &&
116
+ !(path || source.root).endsWith(".zst")) {
117
+ const handle = await open(await selectedPath(source, path, false), "r");
118
+ try {
119
+ const buffer = Buffer.alloc(256 * 1024), read = await handle.read(buffer, 0, buffer.length, 0);
120
+ const prefix = buffer.subarray(0, read.bytesRead).toString("utf8"), end = prefix.lastIndexOf("\n");
121
+ text = prefix;
122
+ if (!prefix.endsWith("\n")) {
123
+ try {
124
+ JSON.parse(prefix.slice(end + 1));
125
+ }
126
+ catch {
127
+ text = prefix.slice(0, end + 1);
128
+ }
129
+ }
130
+ }
131
+ finally {
132
+ await handle.close();
133
+ }
134
+ }
135
+ else
136
+ text = await readSelectedFile(source, path);
137
+ const rows = source.acquisition === "bundle" || sessionFileJson(source, path)
138
+ ? [object(JSON.parse(text))]
139
+ : text
140
+ .split(/\r?\n/u)
141
+ .filter(Boolean)
142
+ .slice(0, source.source === "claude_code" ? undefined : 12)
143
+ .map((line) => object(JSON.parse(line)));
144
+ const header = rows.find((row) => row.type === "session" || row.type === "session_meta") ??
145
+ rows[0] ??
146
+ {};
147
+ const payload = header.type === "session_meta"
148
+ ? object(header.payload)
149
+ : source.source === "grok_build"
150
+ ? object(header.params)
151
+ : source.source === "opencode"
152
+ ? object(header.info)
153
+ : header;
154
+ const id = typeof payload.id === "string"
155
+ ? payload.id
156
+ : typeof payload.sessionId === "string"
157
+ ? payload.sessionId
158
+ : basename(path).replace(/\.jsonl(?:\.zst)?$/u, "");
159
+ // Claude writes queue/history bookkeeping before its first message. The
160
+ // native session's message metadata owns cwd; encoded project-directory
161
+ // names are ambiguous and must never be decoded into a guessed path.
162
+ const claudeMessage = source.source === "claude_code"
163
+ ? rows.find((row) => row.sessionId === id && row.type === "user" && typeof row.cwd === "string" && isAbsolute(row.cwd))
164
+ : undefined;
165
+ let cwd = claudeMessage?.cwd ?? payload.cwd;
166
+ let title = id;
167
+ if (source.source === "grok_build" && path.endsWith("updates.jsonl")) {
168
+ try {
169
+ const summary = object(JSON.parse(await readSelectedFile(source, join(dirname(path), "summary.json"))));
170
+ const info = object(summary.info);
171
+ if (info.id === id) {
172
+ cwd = info.cwd;
173
+ if (typeof summary.generated_title === "string")
174
+ title = summary.generated_title.slice(0, 500);
175
+ }
176
+ }
177
+ catch { /* Missing/unreadable metadata leaves history readable without native resume. */ }
178
+ }
179
+ candidates.push({
180
+ nativeSessionId: id,
181
+ sourceInstanceId: source.instanceId,
182
+ path,
183
+ title,
184
+ cwd: typeof cwd === "string" && isAbsolute(cwd) ? cwd : null,
185
+ updatedAt: modified.mtime.toISOString(),
186
+ storageRevision: [
187
+ modified.dev,
188
+ modified.ino,
189
+ modified.size,
190
+ modified.mtimeMs,
191
+ modified.ctimeMs,
192
+ ].join(":"),
193
+ });
194
+ }
195
+ catch (error) {
196
+ if (error.code === "ENOENT")
197
+ continue;
198
+ candidates.push({
199
+ nativeSessionId: basename(path || source.root),
200
+ sourceInstanceId: source.instanceId,
201
+ path,
202
+ title: basename(path || source.root),
203
+ cwd: null,
204
+ updatedAt: new Date(0).toISOString(),
205
+ issue: error instanceof Error
206
+ ? error.message
207
+ : "Unable to inspect native session",
208
+ });
209
+ }
210
+ if (candidates.length > limit)
211
+ break;
212
+ }
213
+ return {
214
+ items: candidates.slice(0, limit),
215
+ nextCursor: candidates.length > limit ? candidates[limit - 1].path : null,
216
+ };
217
+ }
218
+ export async function inspectSessionBranches(source, session) {
219
+ if (source.source !== "claude_code")
220
+ return { revision: "", branches: [] };
221
+ if (session.issue)
222
+ throw new Error(session.issue);
223
+ if (session.sourceInstanceId !== source.instanceId)
224
+ throw new Error("Native session belongs to another selected source.");
225
+ const text = await readSelectedFile(source, session.path);
226
+ const graph = claudeGraph(connectedJsonLines({ path: basename(session.path), text }));
227
+ if (graph.sessionId !== session.nativeSessionId)
228
+ throw new Error("Native session changed identity.");
229
+ return { revision: contentHash(text), branches: graph.branches() };
230
+ }
231
+ export async function readSession(source, session, input = {}) {
232
+ if (session.issue)
233
+ throw new Error(session.issue);
234
+ if (session.sourceInstanceId !== source.instanceId)
235
+ throw new Error("Native session belongs to another selected source.");
236
+ let files;
237
+ if (source.acquisition === "sqlite")
238
+ files = [readDatabaseSession(source, session.nativeSessionId)];
239
+ else if (source.acquisition === "bundle") {
240
+ const directory = session.path.slice(0, -"manifest.json".length);
241
+ files = await Promise.all(["manifest.json", "events.jsonl", "session-branch.json"].map(async (path) => ({
242
+ path,
243
+ text: await readSelectedFile(source, `${directory}${path}`),
244
+ })));
245
+ }
246
+ else
247
+ files = [
248
+ {
249
+ path: basename(session.path || source.root).replace(/\.zst$/u, ""),
250
+ text: await readSelectedFile(source, session.path),
251
+ },
252
+ ];
253
+ if (input.expectedBranchRevision && !input.branchLeafId)
254
+ throw new Error("Choose a branch before supplying its revision.");
255
+ if (source.source === "claude_code") {
256
+ const file = files[0];
257
+ const graph = claudeGraph(connectedJsonLines(file));
258
+ if (graph.sessionId !== session.nativeSessionId)
259
+ throw new Error("Native session changed identity.");
260
+ if (input.branchAnchor) {
261
+ if (input.branchLeafId || input.expectedBranchRevision)
262
+ throw new Error("Choose either a frozen branch snapshot or a followed branch anchor.");
263
+ const anchor = graph.select(input.branchAnchor.leafId);
264
+ if (anchor.chainHash !== input.branchAnchor.chainHash)
265
+ throw new Error("The selected branch history changed. Review and select it again before syncing.");
266
+ const descendants = graph.leaves.filter(leaf => graph.select(leaf.uuid).chain.some(row => row.uuid === input.branchAnchor.leafId));
267
+ if (descendants.length !== 1)
268
+ throw new Error("The selected branch diverged. Pause this connection and choose its branch again.");
269
+ input = { ...input, branchLeafId: descendants[0].uuid };
270
+ }
271
+ if (input.expectedBranchRevision && contentHash(file.text) !== input.expectedBranchRevision)
272
+ throw new Error("This conversation changed. Refresh its branches and choose again.");
273
+ if (input.branchLeafId) {
274
+ if (!graph.leaves.some(leaf => leaf.uuid === input.branchLeafId))
275
+ throw new Error("The selected Claude branch is no longer a leaf. Refresh and choose again.");
276
+ // Selected-branch consent never uploads sibling transcript rows. The
277
+ // retained native file is unchanged; admission receives only this chain.
278
+ files = [{ ...file, text: graph.select(input.branchLeafId).chain.map(row => JSON.stringify(row)).join("\n") + "\n" }];
279
+ }
280
+ }
281
+ else if (input.expectedBranchRevision || input.branchAnchor)
282
+ throw new Error("Branch revision selection is only supported for Claude Code.");
283
+ const preview = previewAgentImport({
284
+ source: source.source,
285
+ files,
286
+ acquisition: {
287
+ machineId: source.machineId,
288
+ sourceInstanceId: source.instanceId,
289
+ },
290
+ ...input,
291
+ });
292
+ if (preview.issues.length)
293
+ throw new Error(preview.issues.map((issue) => issue.message).join("; "));
294
+ if (source.acquisition === "sqlite" &&
295
+ preview.sessions.some((item) => item.sessionId !== session.nativeSessionId))
296
+ throw new Error("Native session changed identity.");
297
+ return { files, preview, branchLeafId: input.branchLeafId };
298
+ }
@@ -0,0 +1,11 @@
1
+ /** Node-only retained native history and independently supervised sync service. */
2
+ export * from "./contracts.js";
3
+ export * from "./discovery.js";
4
+ export * from "./history.js";
5
+ export * from "./collector-contracts.js";
6
+ export * from "./collector-store.js";
7
+ export * from "./collector.js";
8
+ export * from "./supervisor.js";
9
+ export * from "./location.js";
10
+ export * from "./collector-branches.js";
11
+ export * from "./collector-destinations.js";
@@ -0,0 +1,29 @@
1
+ import { homedir } from "node:os";
2
+ import { join, resolve } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { mkdir, open, readFile } from "node:fs/promises";
5
+ export function collectorDirectory(home = process.env.OPENPOND_HOME || join(homedir(), ".openpond")) {
6
+ return resolve(home, "conversation-importer");
7
+ }
8
+ export async function collectorMachineId(directory = collectorDirectory()) {
9
+ await mkdir(directory, { recursive: true, mode: 0o700 });
10
+ const file = join(directory, "machine-id");
11
+ try {
12
+ const handle = await open(file, "wx", 0o600);
13
+ try {
14
+ await handle.writeFile(randomUUID());
15
+ await handle.sync();
16
+ }
17
+ finally {
18
+ await handle.close();
19
+ }
20
+ }
21
+ catch (error) {
22
+ if (error.code !== "EEXIST")
23
+ throw error;
24
+ }
25
+ const id = (await readFile(file, "utf8")).trim();
26
+ if (!/^[a-f0-9-]{36}$/u.test(id))
27
+ throw new Error("Collector machine identity is invalid; restore its retained state.");
28
+ return id;
29
+ }
@@ -0,0 +1,91 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync, realpathSync, statSync } from "node:fs";
3
+ import { basename, dirname, join, relative, sep } from "node:path";
4
+ import { zstdDecompressSync } from "node:zlib";
5
+ import { NATIVE_READ_LIMIT } from "./contracts.js";
6
+ /** Read the native cold-storage envelope without restoring or modifying its database. */
7
+ export function readOpenClawColdArchive(database, source, sessionId) {
8
+ const metadata = database
9
+ .prepare("SELECT generation,archive_name,archive_sha256,event_count,raw_bytes,archive_bytes,last_seq,storage,length(archive_blob) AS blob_bytes FROM session_transcript_cold_archives WHERE session_id=?")
10
+ .get(sessionId);
11
+ if (!metadata)
12
+ return null;
13
+ if (Number(metadata.archive_bytes) > NATIVE_READ_LIMIT ||
14
+ Number(metadata.raw_bytes) > NATIVE_READ_LIMIT ||
15
+ Number(metadata.event_count) > 100000)
16
+ throw new Error("OpenClaw cold archive exceeds the retained size limit.");
17
+ let bytes;
18
+ if (metadata.storage === "sqlite") {
19
+ if (Number(metadata.blob_bytes) !== Number(metadata.archive_bytes))
20
+ throw new Error("OpenClaw cold archive size differs from its receipt.");
21
+ const row = database
22
+ .prepare("SELECT archive_blob FROM session_transcript_cold_archives WHERE session_id=?")
23
+ .get(sessionId);
24
+ if (!(row.archive_blob instanceof Uint8Array))
25
+ throw new Error("OpenClaw cold archive is unavailable.");
26
+ bytes = row.archive_blob;
27
+ }
28
+ else if (metadata.storage === "file") {
29
+ const name = String(metadata.archive_name);
30
+ if (!/^[a-f0-9]{64}\.jsonl\.zst$/.test(name))
31
+ throw new Error("Invalid OpenClaw cold archive name.");
32
+ const storeDirectory = dirname(source.root);
33
+ const artifactDirectory = basename(storeDirectory) === "agent"
34
+ ? join(dirname(storeDirectory), "sessions")
35
+ : storeDirectory;
36
+ const allowed = realpathSync(artifactDirectory);
37
+ const path = realpathSync(join(artifactDirectory, "cold", name));
38
+ const within = relative(allowed, path);
39
+ if (within === ".." ||
40
+ within.startsWith(`..${sep}`) ||
41
+ within.startsWith(sep))
42
+ throw new Error("OpenClaw cold archive leaves the selected agent directory.");
43
+ const stat = statSync(path);
44
+ if (!stat.isFile() || stat.size !== Number(metadata.archive_bytes))
45
+ throw new Error("OpenClaw cold archive size differs from its receipt.");
46
+ bytes = readFileSync(path);
47
+ }
48
+ else
49
+ throw new Error("Unsupported OpenClaw cold archive storage.");
50
+ if (bytes.length !== Number(metadata.archive_bytes) ||
51
+ createHash("sha256").update(bytes).digest("hex") !== metadata.archive_sha256)
52
+ throw new Error("OpenClaw cold archive checksum differs from its receipt.");
53
+ const decoded = zstdDecompressSync(bytes, {
54
+ maxOutputLength: NATIVE_READ_LIMIT,
55
+ });
56
+ const lines = new TextDecoder("utf8", { fatal: true })
57
+ .decode(decoded)
58
+ .trimEnd()
59
+ .split("\n");
60
+ const header = JSON.parse(lines.shift() ?? "null");
61
+ if (header?.kind !== "header" ||
62
+ header.version !== 1 ||
63
+ header.sessionId !== sessionId ||
64
+ header.generation !== metadata.generation)
65
+ throw new Error("OpenClaw cold archive identity differs from its receipt.");
66
+ const events = [];
67
+ let lastSequence = -1;
68
+ let rawBytes = 0;
69
+ for (const line of lines) {
70
+ const record = JSON.parse(line);
71
+ if (!["event", "identity", "active", "index", "fts"].includes(record?.kind))
72
+ throw new Error("Unsupported OpenClaw cold archive record.");
73
+ if (record.kind !== "event")
74
+ continue;
75
+ if (!Number.isSafeInteger(record.row?.seq) ||
76
+ record.row.seq <= lastSequence ||
77
+ typeof record.row.event_json !== "string")
78
+ throw new Error("Invalid OpenClaw cold archive event sequence.");
79
+ lastSequence = record.row.seq;
80
+ rawBytes += Buffer.byteLength(record.row.event_json);
81
+ events.push(record.row.event_json);
82
+ }
83
+ if (events.length !== Number(metadata.event_count) ||
84
+ lastSequence !== Number(metadata.last_seq) ||
85
+ rawBytes + events.length - 1 !== Number(metadata.raw_bytes))
86
+ throw new Error("OpenClaw cold archive contents differ from its receipt.");
87
+ const nativeHeader = JSON.parse(events[0] ?? "null");
88
+ if (nativeHeader?.type !== "session" || nativeHeader.id !== sessionId)
89
+ throw new Error("OpenClaw transcript identity differs from selected session.");
90
+ return { path: `${sessionId}.jsonl`, text: events.join("\n") + "\n" };
91
+ }
@@ -0,0 +1,130 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import { zstdDecompressSync } from "node:zlib";
3
+ import { createHash } from "node:crypto";
4
+ import { readOpenClawColdArchive } from "./openclaw-cold-archive.js";
5
+ import { NATIVE_READ_LIMIT, } from "./contracts.js";
6
+ function open(source) {
7
+ const database = new DatabaseSync(source.root, {
8
+ readOnly: true,
9
+ allowExtension: false,
10
+ enableDoubleQuotedStringLiterals: false,
11
+ });
12
+ database.exec("PRAGMA query_only=ON; PRAGMA busy_timeout=1000; BEGIN");
13
+ return database;
14
+ }
15
+ function has(database, table) {
16
+ return !!database
17
+ .prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?")
18
+ .get(table);
19
+ }
20
+ export function listOpenClawSessions(source, input) {
21
+ const database = open(source);
22
+ try {
23
+ // Every hot transcript retains a native session header, including prior reset windows.
24
+ const archiveUnion = has(database, "session_transcript_archives")
25
+ ? "UNION ALL SELECT session_id AS id,created_at AS updated,archive_sha256 AS revision FROM session_transcript_archives"
26
+ : "";
27
+ const coldUnion = has(database, "session_transcript_cold_archives")
28
+ ? "UNION ALL SELECT session_id AS id,archived_at AS updated,archive_sha256 AS revision FROM session_transcript_cold_archives"
29
+ : "";
30
+ const rows = database
31
+ .prepare(`SELECT id,MAX(updated) AS updated,MAX(revision) AS revision FROM (
32
+ SELECT session_id AS id,MAX(created_at) AS updated,CAST(MAX(seq) AS TEXT) AS revision FROM transcript_events GROUP BY session_id
33
+ ${archiveUnion} ${coldUnion}) WHERE id > ? GROUP BY id ORDER BY id LIMIT ?`)
34
+ .all(input.cursor ?? "", input.limit + 1);
35
+ const items = rows.slice(0, input.limit).map((row) => ({
36
+ nativeSessionId: String(row.id),
37
+ sourceInstanceId: source.instanceId,
38
+ path: String(row.id),
39
+ title: String(row.id),
40
+ cwd: null,
41
+ updatedAt: new Date(Number(row.updated) || 0).toISOString(),
42
+ }));
43
+ return {
44
+ items,
45
+ nextCursor: rows.length > input.limit ? items.at(-1).nativeSessionId : null,
46
+ };
47
+ }
48
+ finally {
49
+ database.close();
50
+ }
51
+ }
52
+ export function readOpenClawSession(source, sessionId) {
53
+ const database = open(source);
54
+ try {
55
+ if (has(database, "session_transcript_cold_archives")) {
56
+ const cold = readOpenClawColdArchive(database, source, sessionId);
57
+ if (cold)
58
+ return cold;
59
+ }
60
+ const hasHot = database
61
+ .prepare("SELECT 1 FROM transcript_events WHERE session_id=? LIMIT 1")
62
+ .get(sessionId);
63
+ if (!hasHot) {
64
+ if (!has(database, "session_transcript_archives"))
65
+ throw new Error("OpenClaw archive is unavailable.");
66
+ const metadata = database
67
+ .prepare("SELECT archive_name,length(archive_blob) AS bytes FROM session_transcript_archives WHERE session_id=? ORDER BY created_at DESC LIMIT 1")
68
+ .get(sessionId);
69
+ if (!metadata || Number(metadata.bytes) > NATIVE_READ_LIMIT)
70
+ throw new Error("OpenClaw archive is missing or exceeds the retained byte limit.");
71
+ const archive = database
72
+ .prepare("SELECT archive_blob,archive_sha256,encoding FROM session_transcript_archives WHERE archive_name=? AND session_id=?")
73
+ .get(String(metadata.archive_name), sessionId);
74
+ if (!(archive.archive_blob instanceof Uint8Array) ||
75
+ createHash("sha256").update(archive.archive_blob).digest("hex") !==
76
+ archive.archive_sha256)
77
+ throw new Error("OpenClaw archive checksum differs from its retained receipt.");
78
+ if (archive.encoding !== "zstd" && archive.encoding !== "identity")
79
+ throw new Error("Unsupported OpenClaw archive encoding.");
80
+ const bytes = archive.encoding === "zstd"
81
+ ? zstdDecompressSync(archive.archive_blob, {
82
+ maxOutputLength: NATIVE_READ_LIMIT,
83
+ })
84
+ : archive.archive_blob;
85
+ const text = new TextDecoder("utf8", { fatal: true }).decode(bytes);
86
+ const header = JSON.parse(text.split("\n", 1)[0]);
87
+ if (header.type !== "session" || header.id !== sessionId)
88
+ throw new Error("OpenClaw archive identity differs from selected session.");
89
+ return { path: `${sessionId}.jsonl`, text };
90
+ }
91
+ const total = database
92
+ .prepare("SELECT COUNT(*) AS n,SUM(COALESCE(event_utf8_bytes,length(CAST(event_json AS BLOB)),0)) AS bytes FROM transcript_events WHERE session_id=?")
93
+ .get(sessionId);
94
+ if (Number(total.n) > 100000 || Number(total.bytes) > NATIVE_READ_LIMIT)
95
+ throw new Error("OpenClaw transcript exceeds the retained size limit.");
96
+ const rows = database
97
+ .prepare("SELECT seq,event_json,event_zstd,event_utf8_bytes FROM transcript_events WHERE session_id=? ORDER BY seq LIMIT 100001")
98
+ .all(sessionId);
99
+ let bytes = 0;
100
+ const lines = rows.map((row) => {
101
+ let text;
102
+ if (typeof row.event_json === "string")
103
+ text = row.event_json;
104
+ else {
105
+ if (!(row.event_zstd instanceof Uint8Array) ||
106
+ Number(row.event_utf8_bytes) > NATIVE_READ_LIMIT)
107
+ throw new Error("Unsupported OpenClaw compressed transcript record.");
108
+ const decoded = zstdDecompressSync(row.event_zstd, {
109
+ maxOutputLength: NATIVE_READ_LIMIT,
110
+ });
111
+ if (decoded.length !== Number(row.event_utf8_bytes))
112
+ throw new Error("OpenClaw compressed transcript size differs from its receipt.");
113
+ text = new TextDecoder("utf8", { fatal: true }).decode(decoded);
114
+ }
115
+ bytes += Buffer.byteLength(text);
116
+ if (bytes > NATIVE_READ_LIMIT)
117
+ throw new Error("OpenClaw transcript exceeds retained size limit.");
118
+ return text;
119
+ });
120
+ if (!lines.length)
121
+ throw new Error("OpenClaw transcript is unavailable.");
122
+ const header = JSON.parse(lines[0]);
123
+ if (header.type !== "session" || header.id !== sessionId)
124
+ throw new Error("OpenClaw transcript identity differs from selected session.");
125
+ return { path: `${sessionId}.jsonl`, text: lines.join("\n") + "\n" };
126
+ }
127
+ finally {
128
+ database.close();
129
+ }
130
+ }