@openpond/evals 0.11.18 → 0.11.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/dist/connected-evidence/contracts.js +15 -5
  2. package/dist/connected-evidence/imports.js +78 -17
  3. package/dist/connected-evidence/normalize.js +18 -7
  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 +251 -0
  26. package/dist/native-conversations/collector-sync-requests.js +52 -0
  27. package/dist/native-conversations/collector.js +329 -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 +327 -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/readiness.js +4 -0
  37. package/dist/native-conversations/source-locations.js +112 -0
  38. package/dist/native-conversations/supervisor.js +208 -0
  39. package/dist/native-conversations/titles.js +38 -0
  40. package/dist/rewards.js +1 -1
  41. package/dist/types/connected-evidence/contracts.d.ts +15 -0
  42. package/dist/types/connected-evidence/contracts.d.ts.map +1 -1
  43. package/dist/types/connected-evidence/imports.d.ts +7 -0
  44. package/dist/types/connected-evidence/imports.d.ts.map +1 -1
  45. package/dist/types/connected-evidence/normalize.d.ts +7 -0
  46. package/dist/types/connected-evidence/normalize.d.ts.map +1 -1
  47. package/dist/types/connected-evidence/sources/claude-branches.d.ts +19 -0
  48. package/dist/types/connected-evidence/sources/claude-branches.d.ts.map +1 -0
  49. package/dist/types/connected-evidence/sources/claude-code.d.ts +5 -1
  50. package/dist/types/connected-evidence/sources/claude-code.d.ts.map +1 -1
  51. package/dist/types/connected-evidence/sources/codex-authorization.d.ts +18 -0
  52. package/dist/types/connected-evidence/sources/codex-authorization.d.ts.map +1 -0
  53. package/dist/types/connected-evidence/sources/codex.d.ts +5 -1
  54. package/dist/types/connected-evidence/sources/codex.d.ts.map +1 -1
  55. package/dist/types/connected-evidence/sources/grok-build.d.ts +79 -0
  56. package/dist/types/connected-evidence/sources/grok-build.d.ts.map +1 -0
  57. package/dist/types/connected-evidence/sources/history.d.ts +10 -2
  58. package/dist/types/connected-evidence/sources/history.d.ts.map +1 -1
  59. package/dist/types/connected-evidence/sources/opencode.d.ts +78 -0
  60. package/dist/types/connected-evidence/sources/opencode.d.ts.map +1 -0
  61. package/dist/types/connected-evidence/sources/pi.d.ts +79 -0
  62. package/dist/types/connected-evidence/sources/pi.d.ts.map +1 -0
  63. package/dist/types/graders.d.ts +81 -0
  64. package/dist/types/graders.d.ts.map +1 -1
  65. package/dist/types/learning/budgeted-reward-fixtures.d.ts +2 -1
  66. package/dist/types/learning/budgeted-reward-fixtures.d.ts.map +1 -1
  67. package/dist/types/learning/contracts.d.ts +167 -0
  68. package/dist/types/learning/contracts.d.ts.map +1 -1
  69. package/dist/types/learning/index.d.ts +1 -0
  70. package/dist/types/learning/index.d.ts.map +1 -1
  71. package/dist/types/learning/judge-budget.d.ts +3 -0
  72. package/dist/types/learning/judge-budget.d.ts.map +1 -1
  73. package/dist/types/learning/model-judge.d.ts +6 -1
  74. package/dist/types/learning/model-judge.d.ts.map +1 -1
  75. package/dist/types/learning/operations.d.ts +44 -0
  76. package/dist/types/learning/operations.d.ts.map +1 -1
  77. package/dist/types/learning/repository.d.ts +81 -0
  78. package/dist/types/learning/repository.d.ts.map +1 -1
  79. package/dist/types/learning/reward-calibration-closure.d.ts +2323 -0
  80. package/dist/types/learning/reward-calibration-closure.d.ts.map +1 -0
  81. package/dist/types/learning/reward-calibration.d.ts +1 -1
  82. package/dist/types/learning/reward-calibration.d.ts.map +1 -1
  83. package/dist/types/learning/reward-checks.d.ts +35 -0
  84. package/dist/types/learning/reward-checks.d.ts.map +1 -1
  85. package/dist/types/learning/transport.d.ts +103 -0
  86. package/dist/types/learning/transport.d.ts.map +1 -1
  87. package/dist/types/native-conversations/appimage-runtime.d.ts +12 -0
  88. package/dist/types/native-conversations/appimage-runtime.d.ts.map +1 -0
  89. package/dist/types/native-conversations/collector-branches.d.ts +22 -0
  90. package/dist/types/native-conversations/collector-branches.d.ts.map +1 -0
  91. package/dist/types/native-conversations/collector-contracts.d.ts +92 -0
  92. package/dist/types/native-conversations/collector-contracts.d.ts.map +1 -0
  93. package/dist/types/native-conversations/collector-destinations.d.ts +18 -0
  94. package/dist/types/native-conversations/collector-destinations.d.ts.map +1 -0
  95. package/dist/types/native-conversations/collector-errors.d.ts +13 -0
  96. package/dist/types/native-conversations/collector-errors.d.ts.map +1 -0
  97. package/dist/types/native-conversations/collector-progress.d.ts +24 -0
  98. package/dist/types/native-conversations/collector-progress.d.ts.map +1 -0
  99. package/dist/types/native-conversations/collector-store.d.ts +31 -0
  100. package/dist/types/native-conversations/collector-store.d.ts.map +1 -0
  101. package/dist/types/native-conversations/collector-sync-requests.d.ts +25 -0
  102. package/dist/types/native-conversations/collector-sync-requests.d.ts.map +1 -0
  103. package/dist/types/native-conversations/collector.d.ts +8 -0
  104. package/dist/types/native-conversations/collector.d.ts.map +1 -0
  105. package/dist/types/native-conversations/contracts.d.ts +47 -0
  106. package/dist/types/native-conversations/contracts.d.ts.map +1 -0
  107. package/dist/types/native-conversations/database.d.ts +14 -0
  108. package/dist/types/native-conversations/database.d.ts.map +1 -0
  109. package/dist/types/native-conversations/discovery.d.ts +9 -0
  110. package/dist/types/native-conversations/discovery.d.ts.map +1 -0
  111. package/dist/types/native-conversations/history.d.ts +23 -0
  112. package/dist/types/native-conversations/history.d.ts.map +1 -0
  113. package/dist/types/native-conversations/index.d.ts +12 -0
  114. package/dist/types/native-conversations/index.d.ts.map +1 -0
  115. package/dist/types/native-conversations/location.d.ts +3 -0
  116. package/dist/types/native-conversations/location.d.ts.map +1 -0
  117. package/dist/types/native-conversations/openclaw-cold-archive.d.ts +8 -0
  118. package/dist/types/native-conversations/openclaw-cold-archive.d.ts.map +1 -0
  119. package/dist/types/native-conversations/openclaw-database.d.ts +14 -0
  120. package/dist/types/native-conversations/openclaw-database.d.ts.map +1 -0
  121. package/dist/types/native-conversations/readiness.d.ts +5 -0
  122. package/dist/types/native-conversations/readiness.d.ts.map +1 -0
  123. package/dist/types/native-conversations/source-locations.d.ts +8 -0
  124. package/dist/types/native-conversations/source-locations.d.ts.map +1 -0
  125. package/dist/types/native-conversations/supervisor.d.ts +21 -0
  126. package/dist/types/native-conversations/supervisor.d.ts.map +1 -0
  127. package/dist/types/native-conversations/titles.d.ts +4 -0
  128. package/dist/types/native-conversations/titles.d.ts.map +1 -0
  129. package/dist/types/rewards.d.ts +50 -0
  130. package/dist/types/rewards.d.ts.map +1 -1
  131. package/dist/types/telemetry-analysis.d.ts +1 -1
  132. package/dist/types/telemetry-bundle.d.ts +2 -2
  133. package/dist/types/telemetry-catalog.d.ts +21 -21
  134. package/dist/types/telemetry.d.ts +2 -2
  135. package/package.json +11 -3
  136. package/schemas/learning/v1/command-request.schema.json +122 -91
  137. package/schemas/learning/v1/decision.schema.json +63 -0
  138. package/schemas/learning/v1/grade.schema.json +91 -0
  139. package/schemas/learning/v1/reward_check.schema.json +91 -0
  140. package/schemas/learning/v1/source.schema.json +28 -0
@@ -0,0 +1,135 @@
1
+ import { homedir } from "node:os";
2
+ import { realpath, stat, readdir, readFile } from "node:fs/promises";
3
+ import { resolve, join, isAbsolute } from "node:path";
4
+ import { contentHash } from "@openpond/harness";
5
+ import { hermesHome, hermesLocations, ompLocations } from "./source-locations.js";
6
+ import { NATIVE_SOURCE_NAMES, } from "./contracts.js";
7
+ /** Discovery returns locations/capabilities only. Transcript reads require a selected source. */
8
+ export async function discoverSources(input) {
9
+ const home = input.home ?? homedir(), env = input.environment ?? process.env;
10
+ const defaults = {
11
+ codex: env.CODEX_HOME || join(home, ".codex"),
12
+ claude_code: env.CLAUDE_CONFIG_DIR || join(home, ".claude"),
13
+ hermes: hermesHome(home, env),
14
+ openclaw: env.OPENCLAW_STATE_DIR || join(home, ".openclaw"),
15
+ opencode: join(env.XDG_DATA_HOME || join(home, ".local/share"), "opencode"),
16
+ grok_build: env.GROK_HOME || join(home, ".grok"),
17
+ pi: env.PI_CODING_AGENT_DIR || join(home, ".pi/agent"),
18
+ oh_my_pi: join(home, ".omp/agent"),
19
+ };
20
+ // Only documented non-secret source-location settings are read, and never emitted.
21
+ const piHome = defaults.pi;
22
+ let piRelativeSetting = false;
23
+ if (env.PI_CODING_AGENT_SESSION_DIR)
24
+ defaults.pi = resolve(env.PI_CODING_AGENT_SESSION_DIR);
25
+ else if (!input.locations?.pi) {
26
+ try {
27
+ const file = join(piHome, "settings.json");
28
+ if ((await stat(file)).size <= 1024 * 1024) {
29
+ const setting = JSON.parse(await readFile(file, "utf8"));
30
+ if (typeof setting.sessionDir === "string" &&
31
+ setting.sessionDir.trim()) {
32
+ if (isAbsolute(setting.sessionDir))
33
+ defaults.pi = setting.sessionDir;
34
+ else
35
+ piRelativeSetting = true;
36
+ }
37
+ }
38
+ }
39
+ catch {
40
+ /* Missing/invalid settings do not authorize broader filesystem discovery. */
41
+ }
42
+ }
43
+ const result = [];
44
+ for (const source of Object.keys(NATIVE_SOURCE_NAMES)) {
45
+ const locations = input.locations?.[source]
46
+ ? [{ root: input.locations[source] }]
47
+ : source === "hermes"
48
+ ? await hermesLocations(resolve(defaults.hermes))
49
+ : source === "oh_my_pi"
50
+ ? await ompLocations(home, env)
51
+ : [{ root: defaults[source] }];
52
+ for (const location of locations) {
53
+ let root = resolve(location.root), available = false, file = false;
54
+ try {
55
+ root = await realpath(root);
56
+ const info = await stat(root);
57
+ file = info.isFile();
58
+ available = info.isDirectory() || file;
59
+ }
60
+ catch {
61
+ /* Absent or unreadable source is shown explicitly. */
62
+ }
63
+ if (location.reason)
64
+ available = false;
65
+ if (source === "openclaw" && available && !file) {
66
+ const agentsRoot = join(root, "agents");
67
+ const agents = await readdir(agentsRoot, { withFileTypes: true }).catch(() => []);
68
+ const databases = [];
69
+ for (const agent of agents.slice(0, 100)) {
70
+ if (!agent.isDirectory() || agent.isSymbolicLink())
71
+ continue;
72
+ const database = join(agentsRoot, agent.name, "agent", "openclaw-agent.sqlite");
73
+ if (await stat(database)
74
+ .then((info) => info.isFile())
75
+ .catch(() => false))
76
+ databases.push(await realpath(database));
77
+ }
78
+ if (databases.length) {
79
+ for (const database of databases)
80
+ result.push({
81
+ source,
82
+ root: database,
83
+ machineId: input.machineId,
84
+ instanceId: `native-${contentHash([input.machineId, source, database]).slice(0, 40)}`,
85
+ acquisition: "sqlite",
86
+ available: true,
87
+ capabilities: { history: true, live: true, nativeResume: false },
88
+ });
89
+ continue;
90
+ }
91
+ }
92
+ if (source === "pi" && piRelativeSetting && !input.locations?.pi) {
93
+ result.push({
94
+ source,
95
+ root,
96
+ machineId: input.machineId,
97
+ instanceId: `native-${contentHash([input.machineId, source, root]).slice(0, 40)}`,
98
+ acquisition: "files",
99
+ available: false,
100
+ capabilities: { history: false, live: false, nativeResume: false },
101
+ reason: "Pi config uses a working-directory-relative sessionDir. Select its resolved directory with --source-path.",
102
+ });
103
+ continue;
104
+ }
105
+ result.push({
106
+ source,
107
+ root,
108
+ machineId: input.machineId,
109
+ instanceId: `native-${contentHash([input.machineId, source, root]).slice(0, 40)}`,
110
+ acquisition: file && !/\.(?:db|sqlite)$/u.test(root)
111
+ ? "files"
112
+ : source === "hermes" ||
113
+ source === "opencode" ||
114
+ (source === "openclaw" && file)
115
+ ? "sqlite"
116
+ : source === "openclaw"
117
+ ? "bundle"
118
+ : "files",
119
+ available,
120
+ capabilities: {
121
+ history: available,
122
+ live: available && (source !== "openclaw" || file),
123
+ nativeResume: false,
124
+ },
125
+ ...(!available
126
+ ? {
127
+ reason: location.reason ??
128
+ "Source location is missing or unreadable. Select a custom location.",
129
+ }
130
+ : {}),
131
+ });
132
+ }
133
+ }
134
+ return result.filter((item, index) => result.findIndex((candidate) => candidate.instanceId === item.instanceId) === index);
135
+ }
@@ -0,0 +1,327 @@
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 { claudeConversationTitle, retainedConversationTitle } from "./titles.js";
10
+ import { NativeSessionNotReadyError } from "./readiness.js";
11
+ import { NATIVE_READ_LIMIT, } from "./contracts.js";
12
+ async function selectedPath(source, path, boundSize = true) {
13
+ const root = await realpath(source.root), target = await realpath(resolve(root, path));
14
+ const child = relative(root, target);
15
+ if (child.startsWith(`..${sep}`) || child === ".." || isAbsolute(child))
16
+ throw new Error("History path is outside the selected source.");
17
+ if (boundSize && (await stat(target)).size > NATIVE_READ_LIMIT)
18
+ throw new Error("Source file exceeds 32 MiB.");
19
+ return target;
20
+ }
21
+ async function readSelectedFile(source, path) {
22
+ const target = await selectedPath(source, path), bytes = await readFile(target);
23
+ if (bytes.byteLength > NATIVE_READ_LIMIT)
24
+ throw new Error("Source changed beyond the retained limit.");
25
+ const decoded = target.endsWith(".zst")
26
+ ? zstdDecompressSync(bytes, { maxOutputLength: 64 * 1024 * 1024 })
27
+ : bytes;
28
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(decoded);
29
+ // A writer may be appending its last line. Only complete JSONL records enter snapshots.
30
+ if (target.endsWith(".jsonl") && text && !text.endsWith("\n")) {
31
+ const last = text.lastIndexOf("\n");
32
+ try {
33
+ JSON.parse(text.slice(last + 1));
34
+ }
35
+ catch {
36
+ return text.slice(0, last + 1);
37
+ }
38
+ }
39
+ return text;
40
+ }
41
+ async function pathsBelow(source) {
42
+ if ((await lstat(source.root)).isFile())
43
+ return [""];
44
+ const roots = source.source === "codex"
45
+ ? ["sessions", "archived_sessions"]
46
+ : source.source === "grok_build"
47
+ ? ["sessions"]
48
+ : source.source === "claude_code"
49
+ ? ["projects"]
50
+ : source.source === "pi" || source.source === "oh_my_pi"
51
+ ? ["sessions"]
52
+ : [""];
53
+ const paths = [];
54
+ async function walk(directory, depth) {
55
+ if (depth > 12)
56
+ throw new Error("Selected history exceeds directory depth limit.");
57
+ let entries;
58
+ try {
59
+ entries = await readdir(join(source.root, directory), {
60
+ withFileTypes: true,
61
+ });
62
+ }
63
+ catch (error) {
64
+ if (error.code === "ENOENT")
65
+ return;
66
+ throw error;
67
+ }
68
+ for (const entry of entries) {
69
+ const path = join(directory, entry.name);
70
+ if (entry.isSymbolicLink())
71
+ continue;
72
+ if (entry.isDirectory())
73
+ await walk(path, depth + 1);
74
+ else if (entry.isFile() &&
75
+ (source.source === "openclaw"
76
+ ? entry.name === "manifest.json"
77
+ : source.source === "grok_build"
78
+ ? entry.name === "updates.jsonl"
79
+ : /\.jsonl(?:\.zst)?$/u.test(entry.name)))
80
+ paths.push(path);
81
+ if (paths.length > 10000)
82
+ throw new Error("Select a narrower source directory (more than 10,000 history files).");
83
+ }
84
+ }
85
+ for (const root of roots)
86
+ await walk(root, 0);
87
+ // Explicit export directories need not mimic the application's home layout.
88
+ if (!paths.length && roots[0] !== "")
89
+ await walk("", 0);
90
+ return paths.sort();
91
+ }
92
+ function sessionFileJson(source, path) {
93
+ return (path || source.root).endsWith(".json");
94
+ }
95
+ export async function listSessions(source, input = {}) {
96
+ if (!source.available || !source.capabilities.history)
97
+ throw new Error(source.reason ?? "Native history is unavailable.");
98
+ const limit = input.limit ?? 50;
99
+ if (!Number.isInteger(limit) ||
100
+ limit < 1 ||
101
+ limit > 100 ||
102
+ (input.since && !Number.isFinite(Date.parse(input.since))))
103
+ throw new Error("Invalid history page or date range.");
104
+ if (source.acquisition === "sqlite")
105
+ return listDatabaseSessions(source, { ...input, limit });
106
+ const paths = await pathsBelow(source), candidates = [];
107
+ for (const path of paths) {
108
+ if (input.cursor && path <= input.cursor)
109
+ continue;
110
+ try {
111
+ const modified = await stat(join(source.root, path));
112
+ if (input.since && modified.mtimeMs < Date.parse(input.since))
113
+ continue;
114
+ let text;
115
+ if (source.acquisition !== "bundle" &&
116
+ !sessionFileJson(source, path) &&
117
+ source.source !== "hermes" &&
118
+ !(path || source.root).endsWith(".zst")) {
119
+ const handle = await open(await selectedPath(source, path, false), "r");
120
+ try {
121
+ const buffer = Buffer.alloc(256 * 1024), read = await handle.read(buffer, 0, buffer.length, 0);
122
+ const prefix = buffer.subarray(0, read.bytesRead).toString("utf8"), end = prefix.lastIndexOf("\n");
123
+ text = prefix;
124
+ if (!prefix.endsWith("\n")) {
125
+ try {
126
+ JSON.parse(prefix.slice(end + 1));
127
+ }
128
+ catch {
129
+ text = prefix.slice(0, end + 1);
130
+ }
131
+ }
132
+ // Claude appends custom titles and summaries to the end of its log.
133
+ // Read a bounded tail too, without importing the entire conversation.
134
+ const size = (await handle.stat()).size;
135
+ if (source.source === "claude_code" && size > read.bytesRead) {
136
+ const tailStart = Math.max(read.bytesRead, size - buffer.length);
137
+ const tailBuffer = Buffer.alloc(buffer.length + 1);
138
+ const tailRead = await handle.read(tailBuffer, 0, tailBuffer.length, tailStart - 1);
139
+ let tail = tailBuffer.subarray(0, tailRead.bytesRead).toString("utf8");
140
+ const firstEnd = tail.indexOf("\n");
141
+ tail = firstEnd < 0 ? "" : tail.slice(firstEnd + 1);
142
+ if (!tail.endsWith("\n")) {
143
+ const end = tail.lastIndexOf("\n");
144
+ try {
145
+ JSON.parse(tail.slice(end + 1));
146
+ }
147
+ catch {
148
+ tail = tail.slice(0, end + 1);
149
+ }
150
+ }
151
+ text += `\n${tail}`;
152
+ }
153
+ }
154
+ finally {
155
+ await handle.close();
156
+ }
157
+ }
158
+ else
159
+ text = await readSelectedFile(source, path);
160
+ const rows = source.acquisition === "bundle" || sessionFileJson(source, path)
161
+ ? [object(JSON.parse(text))]
162
+ : text
163
+ .split(/\r?\n/u)
164
+ .filter(Boolean)
165
+ .slice(0, source.source === "claude_code" ? undefined : 12)
166
+ .map((line) => object(JSON.parse(line)));
167
+ const header = rows.find((row) => row.type === "session" || row.type === "session_meta") ??
168
+ rows[0] ??
169
+ {};
170
+ const payload = header.type === "session_meta"
171
+ ? object(header.payload)
172
+ : source.source === "grok_build"
173
+ ? object(header.params)
174
+ : source.source === "opencode"
175
+ ? object(header.info)
176
+ : header;
177
+ const id = source.source === "claude_code"
178
+ ? basename(path || source.root).replace(/\.jsonl(?:\.zst)?$/u, "")
179
+ : typeof payload.id === "string"
180
+ ? payload.id
181
+ : typeof payload.sessionId === "string"
182
+ ? payload.sessionId
183
+ : basename(path).replace(/\.jsonl(?:\.zst)?$/u, "");
184
+ // Claude writes queue/history bookkeeping before its first message. The
185
+ // native session's message metadata owns cwd; encoded project-directory
186
+ // names are ambiguous and must never be decoded into a guessed path.
187
+ const claudeMessage = source.source === "claude_code"
188
+ ? rows.find((row) => row.sessionId === id && row.type === "user" && typeof row.cwd === "string" && isAbsolute(row.cwd))
189
+ : undefined;
190
+ let cwd = claudeMessage?.cwd ?? payload.cwd;
191
+ let title = source.source === "claude_code"
192
+ ? claudeConversationTitle(rows, id)
193
+ : retainedConversationTitle(payload.title, id);
194
+ if (source.source === "grok_build" && path.endsWith("updates.jsonl")) {
195
+ try {
196
+ const summary = object(JSON.parse(await readSelectedFile(source, join(dirname(path), "summary.json"))));
197
+ const info = object(summary.info);
198
+ if (info.id === id) {
199
+ cwd = info.cwd;
200
+ if (typeof summary.generated_title === "string")
201
+ title = summary.generated_title.slice(0, 500);
202
+ }
203
+ }
204
+ catch { /* Missing/unreadable metadata leaves history readable without native resume. */ }
205
+ }
206
+ candidates.push({
207
+ nativeSessionId: id,
208
+ sourceInstanceId: source.instanceId,
209
+ path,
210
+ title,
211
+ cwd: typeof cwd === "string" && isAbsolute(cwd) ? cwd : null,
212
+ updatedAt: modified.mtime.toISOString(),
213
+ storageRevision: [
214
+ modified.dev,
215
+ modified.ino,
216
+ modified.size,
217
+ modified.mtimeMs,
218
+ modified.ctimeMs,
219
+ ].join(":"),
220
+ });
221
+ }
222
+ catch (error) {
223
+ if (error.code === "ENOENT")
224
+ continue;
225
+ candidates.push({
226
+ nativeSessionId: basename(path || source.root),
227
+ sourceInstanceId: source.instanceId,
228
+ path,
229
+ title: basename(path || source.root),
230
+ cwd: null,
231
+ updatedAt: new Date(0).toISOString(),
232
+ issue: error instanceof Error
233
+ ? error.message
234
+ : "Unable to inspect native session",
235
+ });
236
+ }
237
+ if (candidates.length > limit)
238
+ break;
239
+ }
240
+ return {
241
+ items: candidates.slice(0, limit),
242
+ nextCursor: candidates.length > limit ? candidates[limit - 1].path : null,
243
+ };
244
+ }
245
+ export async function inspectSessionBranches(source, session) {
246
+ if (source.source !== "claude_code")
247
+ return { revision: "", branches: [] };
248
+ if (session.issue)
249
+ throw new Error(session.issue);
250
+ if (session.sourceInstanceId !== source.instanceId)
251
+ throw new Error("Native session belongs to another selected source.");
252
+ const text = await readSelectedFile(source, session.path);
253
+ const graph = claudeGraph(connectedJsonLines({ path: basename(session.path), text }));
254
+ if (graph.sessionId !== session.nativeSessionId)
255
+ throw new Error("Native session changed identity.");
256
+ return { revision: contentHash(text), branches: graph.branches() };
257
+ }
258
+ export async function readSession(source, session, input = {}) {
259
+ if (session.issue)
260
+ throw new Error(session.issue);
261
+ if (session.sourceInstanceId !== source.instanceId)
262
+ throw new Error("Native session belongs to another selected source.");
263
+ let files;
264
+ if (source.acquisition === "sqlite")
265
+ files = [readDatabaseSession(source, session.nativeSessionId)];
266
+ else if (source.acquisition === "bundle") {
267
+ const directory = session.path.slice(0, -"manifest.json".length);
268
+ files = await Promise.all(["manifest.json", "events.jsonl", "session-branch.json"].map(async (path) => ({
269
+ path,
270
+ text: await readSelectedFile(source, `${directory}${path}`),
271
+ })));
272
+ }
273
+ else
274
+ files = [
275
+ {
276
+ path: basename(session.path || source.root).replace(/\.zst$/u, ""),
277
+ text: await readSelectedFile(source, session.path),
278
+ },
279
+ ];
280
+ if (input.expectedBranchRevision && !input.branchLeafId)
281
+ throw new Error("Choose a branch before supplying its revision.");
282
+ if (source.source === "claude_code") {
283
+ const file = files[0];
284
+ const graph = claudeGraph(connectedJsonLines(file));
285
+ if (graph.sessionId !== session.nativeSessionId)
286
+ throw new Error("Native session changed identity.");
287
+ if (input.branchAnchor) {
288
+ if (input.branchLeafId || input.expectedBranchRevision)
289
+ throw new Error("Choose either a frozen branch snapshot or a followed branch anchor.");
290
+ const anchor = graph.select(input.branchAnchor.leafId);
291
+ if (anchor.chainHash !== input.branchAnchor.chainHash)
292
+ throw new Error("The selected branch history changed. Review and select it again before syncing.");
293
+ const descendants = graph.leaves.filter(leaf => graph.select(leaf.uuid).chain.some(row => row.uuid === input.branchAnchor.leafId));
294
+ if (descendants.length !== 1)
295
+ throw new Error("The selected branch diverged. Pause this connection and choose its branch again.");
296
+ input = { ...input, branchLeafId: descendants[0].uuid };
297
+ }
298
+ if (input.expectedBranchRevision && contentHash(file.text) !== input.expectedBranchRevision)
299
+ throw new Error("This conversation changed. Refresh its branches and choose again.");
300
+ if (input.branchLeafId) {
301
+ if (!graph.leaves.some(leaf => leaf.uuid === input.branchLeafId))
302
+ throw new Error("The selected Claude branch is no longer a leaf. Refresh and choose again.");
303
+ // Selected-branch consent never uploads sibling transcript rows. The
304
+ // retained native file is unchanged; admission receives only this chain.
305
+ files = [{ ...file, text: graph.select(input.branchLeafId).chain.map(row => JSON.stringify(row)).join("\n") + "\n" }];
306
+ }
307
+ }
308
+ else if (input.expectedBranchRevision || input.branchAnchor)
309
+ throw new Error("Branch revision selection is only supported for Claude Code.");
310
+ const preview = previewAgentImport({
311
+ source: source.source,
312
+ files,
313
+ acquisition: {
314
+ machineId: source.machineId,
315
+ sourceInstanceId: source.instanceId,
316
+ },
317
+ ...input,
318
+ });
319
+ if (preview.issues.length && preview.issues.every(issue => issue.code === "no_user_request"))
320
+ throw new NativeSessionNotReadyError();
321
+ if (preview.issues.length)
322
+ throw new Error(preview.issues.map((issue) => issue.message).join("; "));
323
+ if (source.acquisition === "sqlite" &&
324
+ preview.sessions.some((item) => item.sessionId !== session.nativeSessionId))
325
+ throw new Error("Native session changed identity.");
326
+ return { files, preview, branchLeafId: input.branchLeafId };
327
+ }
@@ -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
+ }