@openpond/evals 0.11.19 → 0.11.21

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 (83) hide show
  1. package/dist/connected-evidence/collector-coverage-contracts.js +160 -0
  2. package/dist/connected-evidence/contracts.js +1 -1
  3. package/dist/connected-evidence/imports.js +2 -1
  4. package/dist/connected-evidence/index.js +1 -0
  5. package/dist/connected-evidence/normalize.js +14 -4
  6. package/dist/execution-contracts.js +1 -1
  7. package/dist/javascript-isolate-node.js +2 -1
  8. package/dist/javascript-isolate-process-source.js +1 -1
  9. package/dist/javascript-isolate-process.js +2 -1
  10. package/dist/javascript-isolate.js +2 -1
  11. package/dist/javascript-verifier-worker-source.js +1 -1
  12. package/dist/native-conversations/codex-window-inventory.js +97 -0
  13. package/dist/native-conversations/collector-acquisition.js +149 -0
  14. package/dist/native-conversations/collector-branches.js +1 -0
  15. package/dist/native-conversations/collector-calendar-gate.js +19 -0
  16. package/dist/native-conversations/collector-contracts.js +4 -3
  17. package/dist/native-conversations/collector-coverage.js +100 -0
  18. package/dist/native-conversations/collector-cron.js +43 -0
  19. package/dist/native-conversations/collector-schedule.js +61 -0
  20. package/dist/native-conversations/collector-service-files.js +51 -0
  21. package/dist/native-conversations/collector-snapshots.js +70 -0
  22. package/dist/native-conversations/collector-source-cache.js +41 -0
  23. package/dist/native-conversations/collector-store.js +47 -8
  24. package/dist/native-conversations/collector.js +189 -254
  25. package/dist/native-conversations/history.js +44 -6
  26. package/dist/native-conversations/index.js +1 -0
  27. package/dist/native-conversations/location.js +4 -2
  28. package/dist/native-conversations/readiness.js +4 -0
  29. package/dist/native-conversations/supervisor.js +131 -81
  30. package/dist/native-conversations/titles.js +38 -0
  31. package/dist/sql-execution-node.js +2 -1
  32. package/dist/tasksets.js +1 -1
  33. package/dist/types/connected-evidence/collector-coverage-contracts.d.ts +98 -0
  34. package/dist/types/connected-evidence/collector-coverage-contracts.d.ts.map +1 -0
  35. package/dist/types/connected-evidence/contracts.d.ts.map +1 -1
  36. package/dist/types/connected-evidence/imports.d.ts +1 -0
  37. package/dist/types/connected-evidence/imports.d.ts.map +1 -1
  38. package/dist/types/connected-evidence/index.d.ts +1 -0
  39. package/dist/types/connected-evidence/index.d.ts.map +1 -1
  40. package/dist/types/connected-evidence/normalize.d.ts +3 -0
  41. package/dist/types/connected-evidence/normalize.d.ts.map +1 -1
  42. package/dist/types/javascript-isolate-node.d.ts.map +1 -1
  43. package/dist/types/javascript-isolate-process.d.ts.map +1 -1
  44. package/dist/types/javascript-isolate.d.ts.map +1 -1
  45. package/dist/types/native-conversations/codex-window-inventory.d.ts +12 -0
  46. package/dist/types/native-conversations/codex-window-inventory.d.ts.map +1 -0
  47. package/dist/types/native-conversations/collector-acquisition.d.ts +17 -0
  48. package/dist/types/native-conversations/collector-acquisition.d.ts.map +1 -0
  49. package/dist/types/native-conversations/collector-branches.d.ts.map +1 -1
  50. package/dist/types/native-conversations/collector-calendar-gate.d.ts +9 -0
  51. package/dist/types/native-conversations/collector-calendar-gate.d.ts.map +1 -0
  52. package/dist/types/native-conversations/collector-contracts.d.ts +45 -5
  53. package/dist/types/native-conversations/collector-contracts.d.ts.map +1 -1
  54. package/dist/types/native-conversations/collector-coverage.d.ts +62 -0
  55. package/dist/types/native-conversations/collector-coverage.d.ts.map +1 -0
  56. package/dist/types/native-conversations/collector-cron.d.ts +20 -0
  57. package/dist/types/native-conversations/collector-cron.d.ts.map +1 -0
  58. package/dist/types/native-conversations/collector-schedule.d.ts +13 -0
  59. package/dist/types/native-conversations/collector-schedule.d.ts.map +1 -0
  60. package/dist/types/native-conversations/collector-service-files.d.ts +20 -0
  61. package/dist/types/native-conversations/collector-service-files.d.ts.map +1 -0
  62. package/dist/types/native-conversations/collector-snapshots.d.ts +25 -0
  63. package/dist/types/native-conversations/collector-snapshots.d.ts.map +1 -0
  64. package/dist/types/native-conversations/collector-source-cache.d.ts +31 -0
  65. package/dist/types/native-conversations/collector-source-cache.d.ts.map +1 -0
  66. package/dist/types/native-conversations/collector-store.d.ts +11 -2
  67. package/dist/types/native-conversations/collector-store.d.ts.map +1 -1
  68. package/dist/types/native-conversations/collector.d.ts +5 -1
  69. package/dist/types/native-conversations/collector.d.ts.map +1 -1
  70. package/dist/types/native-conversations/history.d.ts +7 -0
  71. package/dist/types/native-conversations/history.d.ts.map +1 -1
  72. package/dist/types/native-conversations/index.d.ts +1 -0
  73. package/dist/types/native-conversations/index.d.ts.map +1 -1
  74. package/dist/types/native-conversations/location.d.ts.map +1 -1
  75. package/dist/types/native-conversations/readiness.d.ts +5 -0
  76. package/dist/types/native-conversations/readiness.d.ts.map +1 -0
  77. package/dist/types/native-conversations/supervisor.d.ts +4 -3
  78. package/dist/types/native-conversations/supervisor.d.ts.map +1 -1
  79. package/dist/types/native-conversations/titles.d.ts +4 -0
  80. package/dist/types/native-conversations/titles.d.ts.map +1 -0
  81. package/dist/types/sql-execution-node.d.ts.map +1 -1
  82. package/package.json +4 -3
  83. package/schemas/learning/v1/package.schema.json +0 -1
@@ -1,21 +1,24 @@
1
- import { agentImportNormalizerRevision } from "../connected-evidence/imports.js";
2
- import { watch } from "node:fs";
3
- import { contentHash } from "@openpond/harness";
4
1
  import { randomUUID } from "node:crypto";
5
- import { listSessions, readSession } from "./history.js";
2
+ import { setTimeout as delay } from "node:timers/promises";
6
3
  import { CollectorStore } from "./collector-store.js";
7
- import { CollectorErrors, } from "./collector-errors.js";
8
- import { collectorBranchAnchor } from "./collector-branches.js";
4
+ import { CollectorSourceCache } from "./collector-source-cache.js";
5
+ import { CollectorErrors } from "./collector-errors.js";
9
6
  import { CollectorSyncRequests } from "./collector-sync-requests.js";
10
- import { COLLECTOR_DEFAULTS, } from "./collector-contracts.js";
11
- /** Supervised single owner: acquisition and admission checkpoints survive monitor/service exits. */
7
+ import { acquireCollectorSource, CollectorInterruptedError, CollectorScopeChangedError } from "./collector-acquisition.js";
8
+ import { scheduledSyncDue, scheduledMinute } from "./collector-schedule.js";
9
+ import { COLLECTOR_DEFAULTS } from "./collector-contracts.js";
10
+ /** A single owned, bounded job. No filesystem watchers or idle network polling. */
12
11
  export async function runCollector(input) {
12
+ const until = input.until ?? new Date().toISOString();
13
+ if (!Number.isFinite(Date.parse(until)) || Date.parse(until) > Date.now())
14
+ throw new Error("--until must be a valid timestamp no later than now.");
13
15
  const store = await CollectorStore.open(input.directory), nonce = randomUUID();
16
+ let selected = [];
14
17
  store.database.exec("BEGIN IMMEDIATE");
15
18
  try {
16
- const retained = store.setting("owner");
17
- if (retained) {
18
- const owner = JSON.parse(retained);
19
+ const retainedOwner = store.setting("owner");
20
+ if (retainedOwner) {
21
+ const owner = JSON.parse(retainedOwner);
19
22
  let alive = true;
20
23
  try {
21
24
  process.kill(owner.pid, 0);
@@ -23,10 +26,32 @@ export async function runCollector(input) {
23
26
  catch (error) {
24
27
  alive = error.code !== "ESRCH";
25
28
  }
26
- if (alive)
27
- throw new Error("The collector already has a live owner.");
29
+ if (alive) {
30
+ if (input.trigger === "scheduled") {
31
+ store.database.exec("COMMIT");
32
+ store.close();
33
+ return;
34
+ }
35
+ throw new Error("An import is already running. Cancel it or wait for it to finish.");
36
+ }
37
+ }
38
+ const now = new Date();
39
+ selected = store.connections().filter(connection => connection.state !== "disconnected" &&
40
+ (!input.connectionIds || input.connectionIds.includes(connection.id)) &&
41
+ (input.trigger !== "scheduled" || (connection.state === "active" && scheduledSyncDue(store.schedule(connection.id), store.setting(`scheduledMinute:${connection.id}`), now))));
42
+ if (input.trigger === "scheduled")
43
+ for (const connection of selected)
44
+ store.set(`scheduledMinute:${connection.id}`, scheduledMinute(now));
45
+ if (selected.length && input.trigger)
46
+ store.set("desiredState", "running");
47
+ if (!selected.length || store.setting("desiredState") !== "running" || input.signal.aborted) {
48
+ store.database.exec("COMMIT");
49
+ store.close();
50
+ return;
28
51
  }
29
52
  store.set("owner", JSON.stringify({ pid: process.pid, nonce }));
53
+ store.set("pid", String(process.pid));
54
+ store.set("heartbeatAt", now.toISOString());
30
55
  store.database.exec("COMMIT");
31
56
  }
32
57
  catch (error) {
@@ -34,33 +59,23 @@ export async function runCollector(input) {
34
59
  store.close();
35
60
  throw error;
36
61
  }
37
- if (store.setting("desiredState") !== "running") {
38
- store.set("owner", "");
39
- store.close();
40
- return;
41
- }
42
- store.set("pid", String(process.pid));
43
- let dirty = true, lastReconcile = 0, lastHeartbeat = 0;
44
- const watchers = new Map();
45
- const errors = new CollectorErrors(store);
46
- const syncRequests = new CollectorSyncRequests(store);
47
- const reconciledRevisions = new Map();
48
- function retained(connection) {
49
- return (store.connections().find((item) => item.id === connection.id) ??
50
- connection);
51
- }
52
- function heartbeatInput(connection) {
53
- const pendingOperations = store.queued(connection.id);
54
- const error = errors.message(connection.id, "heartbeat");
55
- return {
56
- pendingOperations,
57
- error,
58
- // A durable older completion may coexist with newly queued live work.
59
- // Keep its receipt, but do not send an ineligible ACK that the server
60
- // must reject while the current acquisition/admission state is unhealthy.
61
- completedSyncRevision: !pendingOperations && !error ? connection.completedSyncRevision ?? 0 : 0,
62
- };
63
- }
62
+ const controller = new AbortController();
63
+ const deadlineAt = Date.now() + (input.maxDurationMs ?? COLLECTOR_DEFAULTS.runMs);
64
+ const signal = AbortSignal.any([input.signal, controller.signal]);
65
+ const deadline = setTimeout(() => controller.abort(new Error("Import exceeded its one-hour time budget. Remaining work is retained for the next run.")), input.maxDurationMs ?? COLLECTOR_DEFAULTS.runMs);
66
+ // Local cancellation also interrupts an in-flight HTTP request.
67
+ const pulse = setInterval(() => {
68
+ if (store.setting("desiredState") !== "running")
69
+ controller.abort(new CollectorInterruptedError("Import cancelled."));
70
+ else
71
+ store.set("heartbeatAt", new Date().toISOString());
72
+ }, 250);
73
+ const errors = new CollectorErrors(store), syncRequests = new CollectorSyncRequests(store);
74
+ const retained = (connection) => store.connections().find(item => item.id === connection.id) ?? connection;
75
+ const heartbeatInput = (connection) => {
76
+ const pendingOperations = store.queued(connection.id), error = errors.message(connection.id, "heartbeat");
77
+ return { pendingOperations, error, completedSyncRevision: !pendingOperations && !error ? connection.completedSyncRevision ?? 0 : 0 };
78
+ };
64
79
  function applyRemote(connection, remote) {
65
80
  const current = retained(connection);
66
81
  if (current.state === "disconnected" || remote.revision < current.revision)
@@ -73,249 +88,169 @@ export async function runCollector(input) {
73
88
  return next;
74
89
  }
75
90
  function recordFailure(connection, error, phase) {
76
- const message = error instanceof Error ? error.message : "Collection failed.";
77
- errors.set(connection.id, phase, message);
78
- const status = error && typeof error === "object" && "status" in error
79
- ? Number(error.status)
80
- : 0;
91
+ errors.set(connection.id, phase, error instanceof Error ? error.message : "Import failed.");
92
+ const status = error && typeof error === "object" && "status" in error ? Number(error.status) : 0;
81
93
  const current = retained(connection);
82
- // Authentication loss requires explicit reconnection; a timer or restart
83
- // must never turn a revoked connection back into a paused/resumable one.
84
94
  if (current.state !== "disconnected" && [400, 401, 403].includes(status))
85
- store.put({
86
- ...current,
87
- state: status === 400 ? "paused" : "disconnected",
88
- });
95
+ store.put({ ...current, state: status === 400 ? "paused" : "disconnected" });
89
96
  }
90
- async function acquire(connection) {
91
- // A request arriving mid-scan must not be acknowledged by the older scan.
92
- const controlRevision = connection.revision;
93
- const requestedSyncRevision = connection.requestedSyncRevision ?? 0;
94
- let cursor;
95
- const failures = [];
96
- do {
97
- if (Date.now() - lastHeartbeat >= COLLECTOR_DEFAULTS.heartbeatMs) {
98
- lastHeartbeat = Date.now();
99
- store.set("heartbeatAt", new Date().toISOString());
100
- for (const current of store.connections()) {
101
- if (current.state === "disconnected")
102
- continue;
103
- try {
104
- const remote = await input.transport.heartbeat(current, heartbeatInput(current));
105
- applyRemote(current, remote);
106
- errors.set(current.id, "heartbeat", null);
107
- }
108
- catch (error) {
109
- recordFailure(current, error, "heartbeat");
110
- }
111
- }
112
- }
113
- connection =
114
- store.connections().find((item) => item.id === connection.id) ??
115
- connection;
116
- if (connection.state !== "active")
117
- return;
118
- if (input.signal.aborted || store.setting("desiredState") !== "running")
119
- return;
120
- const page = await listSessions(connection.source, {
121
- since: connection.since ?? undefined,
122
- cursor,
123
- limit: 50,
124
- });
125
- for (const native of page.items) {
126
- const scanKey = JSON.stringify([native.path, native.nativeSessionId]);
127
- const scanRevision = native.storageRevision
128
- ? contentHash([native.storageRevision, agentImportNormalizerRevision(connection.source.source)])
129
- : null;
130
- store.progress.select(connection.id, scanKey);
131
- if (input.signal.aborted ||
132
- store.setting("desiredState") !== "running" ||
133
- store.connections().find((item) => item.id === connection.id)
134
- ?.state !== "active")
135
- return;
136
- try {
137
- if (scanRevision &&
138
- !store.sourceChanged(connection.id, scanKey, scanRevision) &&
139
- !store.progress.tracks(connection.id, scanKey))
140
- continue;
141
- const { files, preview, branchLeafId } = await readSession(connection.source, native, {
142
- branchAnchor: collectorBranchAnchor(store, connection.id, native.nativeSessionId),
143
- });
144
- let eligible = false;
145
- for (const session of preview.sessions) {
146
- const key = JSON.stringify([
147
- connection.source.instanceId,
148
- session.sessionId,
149
- session.branchId,
150
- ]);
151
- store.progress.retained(connection.id, scanKey, key);
152
- const selectedBoundaries = session.boundaries.filter((boundary) => boundary.projection === "turn" &&
153
- boundary.terminal !== "unknown" &&
154
- (!connection.since ||
155
- Date.parse(session.events[boundary.start].occurredAt ??
156
- native.updatedAt) >= Date.parse(connection.since)));
157
- eligible ||= selectedBoundaries.length > 0;
158
- const changed = selectedBoundaries.filter((boundary) => store.changed(connection.id, key, boundary.id, contentHash([boundary.inputHash, boundary.revisionHash])));
159
- for (let offset = 0; offset < changed.length; offset += COLLECTOR_DEFAULTS.batchCases) {
160
- const selected = changed.slice(offset, offset + COLLECTOR_DEFAULTS.batchCases);
161
- const operationId = `sync-${contentHash([connection.id, key, selected.map((boundary) => [boundary.id, boundary.inputHash, boundary.revisionHash])]).slice(0, 48)}`;
162
- store.enqueue({
163
- operationId,
164
- connectionId: connection.id,
165
- sessionKey: key,
166
- contentHash: session.contentHash,
167
- files,
168
- boundaryIds: selected.map((boundary) => boundary.id),
169
- ...(branchLeafId ? { branchLeafId } : {}),
170
- }, selected.map((boundary) => ({
171
- id: boundary.id,
172
- revision: contentHash([
173
- boundary.inputHash,
174
- boundary.revisionHash,
175
- ]),
176
- })), scanKey);
177
- }
178
- }
179
- store.progress.read(connection.id, scanKey, eligible);
180
- if (scanRevision)
181
- store.scanned(connection.id, scanKey, scanRevision);
182
- }
183
- catch (error) {
184
- store.progress.failed(connection.id, scanKey, error instanceof Error ? error.message : "Unable to read session");
185
- failures.push(`${native.nativeSessionId}: ${error instanceof Error ? error.message : "Unable to read session"}`);
186
- }
187
- }
188
- cursor = page.nextCursor ?? undefined;
189
- } while (cursor);
190
- store.progress.discovered(connection.id);
191
- reconciledRevisions.set(connection.id, controlRevision);
192
- syncRequests.acquired(connection.id, { controlRevision, requestedSyncRevision, succeeded: failures.length === 0 });
193
- errors.set(connection.id, "source", failures.length
194
- ? `${failures.length} session(s) need attention. ${failures[0]}`
195
- : null);
97
+ function checkCancelled() {
98
+ if (Date.now() >= deadlineAt && !controller.signal.aborted)
99
+ controller.abort(new Error("Import time budget reached. Remaining work is retained for the next run."));
100
+ if (signal.aborted)
101
+ throw signal.reason ?? new CollectorInterruptedError("Import cancelled.");
102
+ if (store.setting("desiredState") !== "running")
103
+ throw new CollectorInterruptedError("Import cancelled.");
196
104
  }
197
105
  try {
198
- while (!input.signal.aborted &&
199
- store.setting("desiredState") === "running") {
200
- const connections = store.connections();
201
- store.set("heartbeatAt", new Date().toISOString());
202
- for (const connection of connections) {
203
- if (connection.state === "disconnected")
204
- continue;
205
- if (!watchers.has(connection.source.root))
206
- try {
207
- const watcher = watch(connection.source.root, { recursive: connection.source.acquisition !== "sqlite" }, () => {
208
- dirty = true;
209
- });
210
- watcher.on("error", () => {
211
- dirty = true;
212
- });
213
- watchers.set(connection.source.root, watcher);
214
- }
215
- catch {
216
- /* Reconciliation remains authoritative when platform watch is unavailable. */
217
- }
218
- }
219
- const heartbeat = Date.now() - lastHeartbeat >= COLLECTOR_DEFAULTS.heartbeatMs;
220
- if (heartbeat)
106
+ await store.snapshots.migrate(() => signal.aborted || Date.now() >= deadlineAt || store.setting("desiredState") !== "running");
107
+ for (let connection of selected) {
108
+ let phase = "heartbeat", lastHeartbeat = 0;
109
+ let run = { id: nonce, state: "running", phase: "discovering", startedAt: new Date().toISOString(), finishedAt: null, processed: 0, discovered: 0, uploaded: 0, error: null };
110
+ const progress = (update) => { run = { ...run, ...update }; store.set(`run:${connection.id}`, JSON.stringify(run)); };
111
+ progress({});
112
+ const heartbeat = async () => {
113
+ checkCancelled();
114
+ phase = "heartbeat";
115
+ connection = retained(connection);
116
+ const remote = await input.transport.heartbeat(connection, heartbeatInput(connection), signal);
117
+ connection = applyRemote(connection, remote);
118
+ errors.set(connection.id, "heartbeat", null);
221
119
  lastHeartbeat = Date.now();
222
- const reconcile = dirty ||
223
- Date.now() - lastReconcile >= COLLECTOR_DEFAULTS.reconcileMs ||
224
- store.setting("syncNow") === "yes";
225
- if (reconcile) {
226
- dirty = false;
227
- lastReconcile = Date.now();
228
- store.set("syncNow", "no");
229
- }
230
- for (let connection of connections) {
120
+ };
121
+ const check = async () => {
122
+ checkCancelled();
123
+ if (Date.now() - lastHeartbeat >= COLLECTOR_DEFAULTS.heartbeatMs)
124
+ await heartbeat();
231
125
  connection = retained(connection);
232
- if (connection.state === "disconnected")
233
- continue;
234
- let phase = "heartbeat";
235
- try {
236
- if (heartbeat) {
237
- const remote = await input.transport.heartbeat(connection, heartbeatInput(connection));
238
- connection = applyRemote(connection, remote);
239
- errors.set(connection.id, "heartbeat", null);
240
- }
241
- if (connection.state !== "active") {
242
- reconciledRevisions.delete(connection.id);
243
- continue;
244
- }
245
- phase = "source";
246
- // Resume must scan before retained one-time completion can pause again,
247
- // even when the ordinary reconcile interval has not elapsed.
248
- if (reconcile || reconciledRevisions.get(connection.id) !== connection.revision || syncRequests.needsAcquisition(connection))
249
- await acquire(connection);
250
- connection =
251
- store.connections().find((item) => item.id === connection.id) ??
252
- connection;
253
- if (connection.state !== "active")
254
- continue;
255
- if (input.signal.aborted || store.setting("desiredState") !== "running")
256
- continue;
257
- phase = "admission";
126
+ if (connection.state !== "active")
127
+ throw new CollectorInterruptedError(`Source ${connection.state}.`);
128
+ };
129
+ const drain = async () => {
130
+ while (store.queued(connection.id)) {
131
+ await check();
258
132
  const next = store.next(connection.id);
259
- if (next)
133
+ if (!next)
134
+ break;
135
+ progress({ phase: "uploading" });
136
+ let admitted = false;
137
+ for (let attempt = 0; attempt < COLLECTOR_DEFAULTS.attemptsPerRun; attempt++) {
138
+ await check();
139
+ if (!store.database.prepare("SELECT 1 FROM pending WHERE id=? AND connection_id=?").get(next.entry.operationId, connection.id)) {
140
+ admitted = true;
141
+ break;
142
+ }
143
+ phase = "admission";
260
144
  try {
261
- await input.transport.admit(connection, next.entry);
145
+ await input.transport.admit(connection, next.entry, signal);
146
+ // Retain a real receipt even if Cancel arrived while the server committed.
262
147
  store.acknowledge(next.entry);
263
148
  errors.set(connection.id, "admission", null);
149
+ progress({ uploaded: run.uploaded + next.entry.boundaryIds.length });
150
+ admitted = true;
151
+ break;
264
152
  }
265
153
  catch (error) {
266
- store.retry(next.entry.operationId, next.attempts);
267
- throw error;
154
+ store.retry(next.entry.operationId, next.attempts + attempt);
155
+ const status = error && typeof error === "object" && "status" in error ? Number(error.status) : 0;
156
+ if (signal.aborted || [400, 401, 403, 409].includes(status) || attempt + 1 >= COLLECTOR_DEFAULTS.attemptsPerRun)
157
+ throw error;
158
+ await delay(1000 * 2 ** attempt, undefined, { signal });
268
159
  }
160
+ }
161
+ if (!admitted)
162
+ throw new Error("Pending import was not acknowledged.");
163
+ }
164
+ };
165
+ try {
166
+ await heartbeat();
167
+ if (connection.state !== "active")
168
+ throw new CollectorInterruptedError(`Source ${connection.state}.`);
169
+ await drain();
170
+ let completedRevision;
171
+ // Remote scope revisions arriving mid-job require a fresh acquisition
172
+ // before acknowledging that request. Bound concurrent reconfiguration.
173
+ for (let pass = 0; pass < 3; pass++) {
174
+ await check();
175
+ const revision = connection.revision, requested = connection.requestedSyncRevision ?? 0;
176
+ phase = "source";
177
+ new CollectorSourceCache(store).seed(connection, until, store.coverage.manifest(connection.id));
178
+ store.coverage.begin(connection, `${nonce}-${pass}`, run.startedAt, until);
179
+ const checkAcquisition = async () => {
180
+ await check();
181
+ phase = "source";
182
+ if (connection.revision !== revision)
183
+ throw new CollectorScopeChangedError("Source settings changed during acquisition.");
184
+ };
185
+ try {
186
+ await acquireCollectorSource({ store, connection, until, check: checkAcquisition,
187
+ drain: async () => { await drain(); await checkAcquisition(); }, progress });
188
+ }
189
+ catch (error) {
190
+ if (error instanceof CollectorScopeChangedError && pass < 2)
191
+ continue;
192
+ throw error;
193
+ }
194
+ errors.set(connection.id, "source", null);
195
+ await drain();
196
+ await input.transport.publishCoverage(connection, store.coverage.finish(connection.id, true), signal);
269
197
  connection = retained(connection);
270
- if (input.signal.aborted || store.setting("desiredState") !== "running" || connection.state !== "active")
198
+ if (connection.revision !== revision) {
199
+ if (pass === 2)
200
+ throw new Error("Source settings changed repeatedly during import. Run Sync now again.");
271
201
  continue;
272
- connection = syncRequests.complete(connection, errors.message(connection.id, "heartbeat"));
273
- if (syncRequests.needsAcknowledgement(connection) && heartbeatInput(connection).completedSyncRevision) {
274
- phase = "heartbeat";
275
- const remote = await input.transport.heartbeat(connection, heartbeatInput(connection));
276
- connection = applyRemote(connection, remote);
277
- errors.set(connection.id, "heartbeat", null);
278
202
  }
279
- if (!input.signal.aborted &&
280
- store.setting("desiredState") === "running" &&
281
- connection.state === "active" &&
282
- !connection.keepSyncing &&
283
- reconciledRevisions.get(connection.id) === connection.revision &&
284
- store.queued(connection.id) === 0 &&
285
- store.progress.status(connection.id).stage === "complete" &&
286
- !errors.source(connection.id) &&
287
- syncRequests.confirmed(connection.id) >= (connection.requestedSyncRevision ?? 0)) {
288
- phase = "control";
289
- if (!input.transport.pause)
290
- throw new Error("This transport cannot acknowledge completion of a one-time import.");
291
- const paused = await input.transport.pause(connection);
292
- applyRemote(connection, paused);
293
- errors.set(connection.id, "control", null);
203
+ syncRequests.acquired(connection.id, { controlRevision: revision, requestedSyncRevision: requested, succeeded: true });
204
+ connection = syncRequests.complete(connection, errors.message(connection.id, "heartbeat"));
205
+ await heartbeat();
206
+ if (connection.revision !== revision || syncRequests.needsAcquisition(connection)) {
207
+ if (pass === 2)
208
+ throw new Error("Source settings changed during completion. Run Sync now again.");
209
+ continue;
294
210
  }
211
+ if (syncRequests.needsAcknowledgement(connection))
212
+ throw new Error("The server has not acknowledged sync completion. Retry with Sync now.");
213
+ completedRevision = revision;
214
+ break;
295
215
  }
296
- catch (error) {
297
- recordFailure(connection, error, phase);
216
+ await check();
217
+ if (connection.revision !== completedRevision)
218
+ throw new CollectorScopeChangedError("Source settings changed before completion. Run Sync now again.");
219
+ progress({ state: "completed", finishedAt: new Date().toISOString() });
220
+ store.set(`lastSuccess:${connection.id}`, run.finishedAt);
221
+ }
222
+ catch (error) {
223
+ const cancelled = error instanceof CollectorInterruptedError || input.signal.aborted || signal.reason instanceof CollectorInterruptedError;
224
+ const reason = signal.aborted ? signal.reason : error;
225
+ const message = reason instanceof Error ? reason.message : "Import cancelled.";
226
+ if (!cancelled)
227
+ recordFailure(connection, reason, phase);
228
+ progress({ state: cancelled ? "cancelled" : "failed", error: message, finishedAt: new Date().toISOString() });
229
+ if (store.coverage.manifest(connection.id)?.jobId.startsWith(`${nonce}-`)) {
230
+ const coverage = store.coverage.finish(connection.id, false);
231
+ if (!signal.aborted && connection.state === "active") {
232
+ try {
233
+ await input.transport.publishCoverage(connection, coverage, signal);
234
+ }
235
+ catch (coverageError) {
236
+ recordFailure(connection, coverageError, "source");
237
+ }
238
+ }
298
239
  }
299
240
  }
300
- await new Promise((resolve) => {
301
- const done = () => {
302
- clearTimeout(timer);
303
- input.signal.removeEventListener("abort", done);
304
- resolve();
305
- };
306
- const timer = setTimeout(done, 1000);
307
- input.signal.addEventListener("abort", done, { once: true });
308
- });
241
+ if (signal.aborted || store.setting("desiredState") !== "running")
242
+ break;
309
243
  }
310
244
  }
311
245
  finally {
312
- for (const watcher of watchers.values())
313
- watcher.close();
246
+ clearInterval(pulse);
247
+ clearTimeout(deadline);
314
248
  const owner = JSON.parse(store.setting("owner") || "{}");
315
249
  if (owner.nonce === nonce) {
316
250
  store.set("pid", "");
317
251
  store.set("heartbeatAt", "");
318
252
  store.set("owner", "");
253
+ store.set("desiredState", "stopped");
319
254
  }
320
255
  store.close();
321
256
  }
@@ -6,6 +6,9 @@ import { connectedJsonLines, object } from "../connected-evidence/normalize.js";
6
6
  import { claudeGraph } from "../connected-evidence/sources/claude-branches.js";
7
7
  import { contentHash } from "@openpond/harness";
8
8
  import { listDatabaseSessions, readDatabaseSession } from "./database.js";
9
+ import { claudeConversationTitle, retainedConversationTitle } from "./titles.js";
10
+ import { NativeSessionNotReadyError } from "./readiness.js";
11
+ import { inspectCodexWindow } from "./codex-window-inventory.js";
9
12
  import { NATIVE_READ_LIMIT, } from "./contracts.js";
10
13
  async function selectedPath(source, path, boundSize = true) {
11
14
  const root = await realpath(source.root), target = await realpath(resolve(root, path));
@@ -36,6 +39,14 @@ async function readSelectedFile(source, path) {
36
39
  }
37
40
  return text;
38
41
  }
42
+ export async function inspectSessionWindow(source, session, input) {
43
+ if (source.source !== "codex" || source.acquisition !== "files" || session.issue
44
+ || !(session.path || source.root).endsWith(".jsonl"))
45
+ return null;
46
+ if (session.sourceInstanceId !== source.instanceId)
47
+ throw new Error("Native session belongs to another selected source.");
48
+ return inspectCodexWindow({ ...input, path: await selectedPath(source, session.path, false), sessionId: session.nativeSessionId });
49
+ }
39
50
  async function pathsBelow(source) {
40
51
  if ((await lstat(source.root)).isFile())
41
52
  return [""];
@@ -127,6 +138,27 @@ export async function listSessions(source, input = {}) {
127
138
  text = prefix.slice(0, end + 1);
128
139
  }
129
140
  }
141
+ // Claude appends custom titles and summaries to the end of its log.
142
+ // Read a bounded tail too, without importing the entire conversation.
143
+ const size = (await handle.stat()).size;
144
+ if (source.source === "claude_code" && size > read.bytesRead) {
145
+ const tailStart = Math.max(read.bytesRead, size - buffer.length);
146
+ const tailBuffer = Buffer.alloc(buffer.length + 1);
147
+ const tailRead = await handle.read(tailBuffer, 0, tailBuffer.length, tailStart - 1);
148
+ let tail = tailBuffer.subarray(0, tailRead.bytesRead).toString("utf8");
149
+ const firstEnd = tail.indexOf("\n");
150
+ tail = firstEnd < 0 ? "" : tail.slice(firstEnd + 1);
151
+ if (!tail.endsWith("\n")) {
152
+ const end = tail.lastIndexOf("\n");
153
+ try {
154
+ JSON.parse(tail.slice(end + 1));
155
+ }
156
+ catch {
157
+ tail = tail.slice(0, end + 1);
158
+ }
159
+ }
160
+ text += `\n${tail}`;
161
+ }
130
162
  }
131
163
  finally {
132
164
  await handle.close();
@@ -151,11 +183,13 @@ export async function listSessions(source, input = {}) {
151
183
  : source.source === "opencode"
152
184
  ? object(header.info)
153
185
  : 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, "");
186
+ const id = source.source === "claude_code"
187
+ ? basename(path || source.root).replace(/\.jsonl(?:\.zst)?$/u, "")
188
+ : typeof payload.id === "string"
189
+ ? payload.id
190
+ : typeof payload.sessionId === "string"
191
+ ? payload.sessionId
192
+ : basename(path).replace(/\.jsonl(?:\.zst)?$/u, "");
159
193
  // Claude writes queue/history bookkeeping before its first message. The
160
194
  // native session's message metadata owns cwd; encoded project-directory
161
195
  // names are ambiguous and must never be decoded into a guessed path.
@@ -163,7 +197,9 @@ export async function listSessions(source, input = {}) {
163
197
  ? rows.find((row) => row.sessionId === id && row.type === "user" && typeof row.cwd === "string" && isAbsolute(row.cwd))
164
198
  : undefined;
165
199
  let cwd = claudeMessage?.cwd ?? payload.cwd;
166
- let title = id;
200
+ let title = source.source === "claude_code"
201
+ ? claudeConversationTitle(rows, id)
202
+ : retainedConversationTitle(payload.title, id);
167
203
  if (source.source === "grok_build" && path.endsWith("updates.jsonl")) {
168
204
  try {
169
205
  const summary = object(JSON.parse(await readSelectedFile(source, join(dirname(path), "summary.json"))));
@@ -289,6 +325,8 @@ export async function readSession(source, session, input = {}) {
289
325
  },
290
326
  ...input,
291
327
  });
328
+ if (preview.issues.length && preview.issues.every(issue => issue.code === "no_user_request"))
329
+ throw new NativeSessionNotReadyError();
292
330
  if (preview.issues.length)
293
331
  throw new Error(preview.issues.map((issue) => issue.message).join("; "));
294
332
  if (source.acquisition === "sqlite" &&
@@ -9,3 +9,4 @@ export * from "./supervisor.js";
9
9
  export * from "./location.js";
10
10
  export * from "./collector-branches.js";
11
11
  export * from "./collector-destinations.js";
12
+ export * from "./collector-schedule.js";
@@ -2,8 +2,10 @@ import { homedir } from "node:os";
2
2
  import { join, resolve } from "node:path";
3
3
  import { randomUUID } from "node:crypto";
4
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");
5
+ export function collectorDirectory(home = join(homedir(), ".openpond")) {
6
+ return process.env.OPENPOND_COLLECTOR_DIR
7
+ ? resolve(process.env.OPENPOND_COLLECTOR_DIR)
8
+ : resolve(home, "conversation-importer");
7
9
  }
8
10
  export async function collectorMachineId(directory = collectorDirectory()) {
9
11
  await mkdir(directory, { recursive: true, mode: 0o700 });
@@ -0,0 +1,4 @@
1
+ /** An empty/incomplete session is discovered history, but not a training case. */
2
+ export class NativeSessionNotReadyError extends Error {
3
+ constructor() { super("This session has no retained user request yet."); }
4
+ }