@openpond/evals 0.11.20 → 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 (69) hide show
  1. package/dist/connected-evidence/collector-coverage-contracts.js +160 -0
  2. package/dist/connected-evidence/index.js +1 -0
  3. package/dist/execution-contracts.js +1 -1
  4. package/dist/javascript-isolate-node.js +2 -1
  5. package/dist/javascript-isolate-process-source.js +1 -1
  6. package/dist/javascript-isolate-process.js +2 -1
  7. package/dist/javascript-isolate.js +2 -1
  8. package/dist/javascript-verifier-worker-source.js +1 -1
  9. package/dist/native-conversations/codex-window-inventory.js +97 -0
  10. package/dist/native-conversations/collector-acquisition.js +149 -0
  11. package/dist/native-conversations/collector-branches.js +1 -0
  12. package/dist/native-conversations/collector-calendar-gate.js +19 -0
  13. package/dist/native-conversations/collector-contracts.js +4 -3
  14. package/dist/native-conversations/collector-coverage.js +100 -0
  15. package/dist/native-conversations/collector-cron.js +43 -0
  16. package/dist/native-conversations/collector-schedule.js +61 -0
  17. package/dist/native-conversations/collector-service-files.js +51 -0
  18. package/dist/native-conversations/collector-snapshots.js +70 -0
  19. package/dist/native-conversations/collector-source-cache.js +41 -0
  20. package/dist/native-conversations/collector-store.js +43 -8
  21. package/dist/native-conversations/collector.js +189 -261
  22. package/dist/native-conversations/history.js +9 -0
  23. package/dist/native-conversations/index.js +1 -0
  24. package/dist/native-conversations/location.js +4 -2
  25. package/dist/native-conversations/supervisor.js +131 -81
  26. package/dist/sql-execution-node.js +2 -1
  27. package/dist/tasksets.js +1 -1
  28. package/dist/types/connected-evidence/collector-coverage-contracts.d.ts +98 -0
  29. package/dist/types/connected-evidence/collector-coverage-contracts.d.ts.map +1 -0
  30. package/dist/types/connected-evidence/index.d.ts +1 -0
  31. package/dist/types/connected-evidence/index.d.ts.map +1 -1
  32. package/dist/types/javascript-isolate-node.d.ts.map +1 -1
  33. package/dist/types/javascript-isolate-process.d.ts.map +1 -1
  34. package/dist/types/javascript-isolate.d.ts.map +1 -1
  35. package/dist/types/native-conversations/codex-window-inventory.d.ts +12 -0
  36. package/dist/types/native-conversations/codex-window-inventory.d.ts.map +1 -0
  37. package/dist/types/native-conversations/collector-acquisition.d.ts +17 -0
  38. package/dist/types/native-conversations/collector-acquisition.d.ts.map +1 -0
  39. package/dist/types/native-conversations/collector-branches.d.ts.map +1 -1
  40. package/dist/types/native-conversations/collector-calendar-gate.d.ts +9 -0
  41. package/dist/types/native-conversations/collector-calendar-gate.d.ts.map +1 -0
  42. package/dist/types/native-conversations/collector-contracts.d.ts +41 -5
  43. package/dist/types/native-conversations/collector-contracts.d.ts.map +1 -1
  44. package/dist/types/native-conversations/collector-coverage.d.ts +62 -0
  45. package/dist/types/native-conversations/collector-coverage.d.ts.map +1 -0
  46. package/dist/types/native-conversations/collector-cron.d.ts +20 -0
  47. package/dist/types/native-conversations/collector-cron.d.ts.map +1 -0
  48. package/dist/types/native-conversations/collector-schedule.d.ts +13 -0
  49. package/dist/types/native-conversations/collector-schedule.d.ts.map +1 -0
  50. package/dist/types/native-conversations/collector-service-files.d.ts +20 -0
  51. package/dist/types/native-conversations/collector-service-files.d.ts.map +1 -0
  52. package/dist/types/native-conversations/collector-snapshots.d.ts +25 -0
  53. package/dist/types/native-conversations/collector-snapshots.d.ts.map +1 -0
  54. package/dist/types/native-conversations/collector-source-cache.d.ts +31 -0
  55. package/dist/types/native-conversations/collector-source-cache.d.ts.map +1 -0
  56. package/dist/types/native-conversations/collector-store.d.ts +11 -2
  57. package/dist/types/native-conversations/collector-store.d.ts.map +1 -1
  58. package/dist/types/native-conversations/collector.d.ts +5 -1
  59. package/dist/types/native-conversations/collector.d.ts.map +1 -1
  60. package/dist/types/native-conversations/history.d.ts +7 -0
  61. package/dist/types/native-conversations/history.d.ts.map +1 -1
  62. package/dist/types/native-conversations/index.d.ts +1 -0
  63. package/dist/types/native-conversations/index.d.ts.map +1 -1
  64. package/dist/types/native-conversations/location.d.ts.map +1 -1
  65. package/dist/types/native-conversations/supervisor.d.ts +4 -3
  66. package/dist/types/native-conversations/supervisor.d.ts.map +1 -1
  67. package/dist/types/sql-execution-node.d.ts.map +1 -1
  68. package/package.json +3 -2
  69. package/schemas/learning/v1/package.schema.json +0 -1
@@ -1,22 +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";
6
- import { NativeSessionNotReadyError } from "./readiness.js";
2
+ import { setTimeout as delay } from "node:timers/promises";
7
3
  import { CollectorStore } from "./collector-store.js";
8
- import { CollectorErrors, } from "./collector-errors.js";
9
- import { collectorBranchAnchor } from "./collector-branches.js";
4
+ import { CollectorSourceCache } from "./collector-source-cache.js";
5
+ import { CollectorErrors } from "./collector-errors.js";
10
6
  import { CollectorSyncRequests } from "./collector-sync-requests.js";
11
- import { COLLECTOR_DEFAULTS, } from "./collector-contracts.js";
12
- /** 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. */
13
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.");
14
15
  const store = await CollectorStore.open(input.directory), nonce = randomUUID();
16
+ let selected = [];
15
17
  store.database.exec("BEGIN IMMEDIATE");
16
18
  try {
17
- const retained = store.setting("owner");
18
- if (retained) {
19
- const owner = JSON.parse(retained);
19
+ const retainedOwner = store.setting("owner");
20
+ if (retainedOwner) {
21
+ const owner = JSON.parse(retainedOwner);
20
22
  let alive = true;
21
23
  try {
22
24
  process.kill(owner.pid, 0);
@@ -24,10 +26,32 @@ export async function runCollector(input) {
24
26
  catch (error) {
25
27
  alive = error.code !== "ESRCH";
26
28
  }
27
- if (alive)
28
- 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;
29
51
  }
30
52
  store.set("owner", JSON.stringify({ pid: process.pid, nonce }));
53
+ store.set("pid", String(process.pid));
54
+ store.set("heartbeatAt", now.toISOString());
31
55
  store.database.exec("COMMIT");
32
56
  }
33
57
  catch (error) {
@@ -35,33 +59,23 @@ export async function runCollector(input) {
35
59
  store.close();
36
60
  throw error;
37
61
  }
38
- if (store.setting("desiredState") !== "running") {
39
- store.set("owner", "");
40
- store.close();
41
- return;
42
- }
43
- store.set("pid", String(process.pid));
44
- let dirty = true, lastReconcile = 0, lastHeartbeat = 0;
45
- const watchers = new Map();
46
- const errors = new CollectorErrors(store);
47
- const syncRequests = new CollectorSyncRequests(store);
48
- const reconciledRevisions = new Map();
49
- function retained(connection) {
50
- return (store.connections().find((item) => item.id === connection.id) ??
51
- connection);
52
- }
53
- function heartbeatInput(connection) {
54
- const pendingOperations = store.queued(connection.id);
55
- const error = errors.message(connection.id, "heartbeat");
56
- return {
57
- pendingOperations,
58
- error,
59
- // A durable older completion may coexist with newly queued live work.
60
- // Keep its receipt, but do not send an ineligible ACK that the server
61
- // must reject while the current acquisition/admission state is unhealthy.
62
- completedSyncRevision: !pendingOperations && !error ? connection.completedSyncRevision ?? 0 : 0,
63
- };
64
- }
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
+ };
65
79
  function applyRemote(connection, remote) {
66
80
  const current = retained(connection);
67
81
  if (current.state === "disconnected" || remote.revision < current.revision)
@@ -74,255 +88,169 @@ export async function runCollector(input) {
74
88
  return next;
75
89
  }
76
90
  function recordFailure(connection, error, phase) {
77
- const message = error instanceof Error ? error.message : "Collection failed.";
78
- errors.set(connection.id, phase, message);
79
- const status = error && typeof error === "object" && "status" in error
80
- ? Number(error.status)
81
- : 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;
82
93
  const current = retained(connection);
83
- // Authentication loss requires explicit reconnection; a timer or restart
84
- // must never turn a revoked connection back into a paused/resumable one.
85
94
  if (current.state !== "disconnected" && [400, 401, 403].includes(status))
86
- store.put({
87
- ...current,
88
- state: status === 400 ? "paused" : "disconnected",
89
- });
95
+ store.put({ ...current, state: status === 400 ? "paused" : "disconnected" });
90
96
  }
91
- async function acquire(connection) {
92
- // A request arriving mid-scan must not be acknowledged by the older scan.
93
- const controlRevision = connection.revision;
94
- const requestedSyncRevision = connection.requestedSyncRevision ?? 0;
95
- let cursor;
96
- const failures = [];
97
- do {
98
- if (Date.now() - lastHeartbeat >= COLLECTOR_DEFAULTS.heartbeatMs) {
99
- lastHeartbeat = Date.now();
100
- store.set("heartbeatAt", new Date().toISOString());
101
- for (const current of store.connections()) {
102
- if (current.state === "disconnected")
103
- continue;
104
- try {
105
- const remote = await input.transport.heartbeat(current, heartbeatInput(current));
106
- applyRemote(current, remote);
107
- errors.set(current.id, "heartbeat", null);
108
- }
109
- catch (error) {
110
- recordFailure(current, error, "heartbeat");
111
- }
112
- }
113
- }
114
- connection =
115
- store.connections().find((item) => item.id === connection.id) ??
116
- connection;
117
- if (connection.state !== "active")
118
- return;
119
- if (input.signal.aborted || store.setting("desiredState") !== "running")
120
- return;
121
- const page = await listSessions(connection.source, {
122
- since: connection.since ?? undefined,
123
- cursor,
124
- limit: 50,
125
- });
126
- for (const native of page.items) {
127
- const scanKey = JSON.stringify([native.path, native.nativeSessionId]);
128
- const scanRevision = native.storageRevision
129
- ? contentHash([native.storageRevision, agentImportNormalizerRevision(connection.source.source)])
130
- : null;
131
- store.progress.select(connection.id, scanKey);
132
- if (input.signal.aborted ||
133
- store.setting("desiredState") !== "running" ||
134
- store.connections().find((item) => item.id === connection.id)
135
- ?.state !== "active")
136
- return;
137
- try {
138
- if (scanRevision &&
139
- !store.sourceChanged(connection.id, scanKey, scanRevision) &&
140
- !store.progress.tracks(connection.id, scanKey))
141
- continue;
142
- const { files, preview, branchLeafId } = await readSession(connection.source, native, {
143
- branchAnchor: collectorBranchAnchor(store, connection.id, native.nativeSessionId),
144
- });
145
- let eligible = false;
146
- for (const session of preview.sessions) {
147
- const key = JSON.stringify([
148
- connection.source.instanceId,
149
- session.sessionId,
150
- session.branchId,
151
- ]);
152
- store.progress.retained(connection.id, scanKey, key);
153
- const selectedBoundaries = session.boundaries.filter((boundary) => boundary.projection === "turn" &&
154
- boundary.terminal !== "unknown" &&
155
- (!connection.since ||
156
- Date.parse(session.events[boundary.start].occurredAt ??
157
- native.updatedAt) >= Date.parse(connection.since)));
158
- eligible ||= selectedBoundaries.length > 0;
159
- const changed = selectedBoundaries.filter((boundary) => store.changed(connection.id, key, boundary.id, contentHash([boundary.inputHash, boundary.revisionHash])));
160
- for (let offset = 0; offset < changed.length; offset += COLLECTOR_DEFAULTS.batchCases) {
161
- const selected = changed.slice(offset, offset + COLLECTOR_DEFAULTS.batchCases);
162
- const operationId = `sync-${contentHash([connection.id, key, selected.map((boundary) => [boundary.id, boundary.inputHash, boundary.revisionHash])]).slice(0, 48)}`;
163
- store.enqueue({
164
- operationId,
165
- connectionId: connection.id,
166
- sessionKey: key,
167
- contentHash: session.contentHash,
168
- files,
169
- boundaryIds: selected.map((boundary) => boundary.id),
170
- ...(branchLeafId ? { branchLeafId } : {}),
171
- }, selected.map((boundary) => ({
172
- id: boundary.id,
173
- revision: contentHash([
174
- boundary.inputHash,
175
- boundary.revisionHash,
176
- ]),
177
- })), scanKey);
178
- }
179
- }
180
- store.progress.read(connection.id, scanKey, eligible);
181
- if (scanRevision)
182
- store.scanned(connection.id, scanKey, scanRevision);
183
- }
184
- catch (error) {
185
- if (error instanceof NativeSessionNotReadyError) {
186
- store.progress.read(connection.id, scanKey, false);
187
- if (scanRevision)
188
- store.scanned(connection.id, scanKey, scanRevision);
189
- continue;
190
- }
191
- store.progress.failed(connection.id, scanKey, error instanceof Error ? error.message : "Unable to read session");
192
- failures.push(`${native.nativeSessionId}: ${error instanceof Error ? error.message : "Unable to read session"}`);
193
- }
194
- }
195
- cursor = page.nextCursor ?? undefined;
196
- } while (cursor);
197
- store.progress.discovered(connection.id);
198
- reconciledRevisions.set(connection.id, controlRevision);
199
- syncRequests.acquired(connection.id, { controlRevision, requestedSyncRevision, succeeded: failures.length === 0 });
200
- errors.set(connection.id, "source", failures.length
201
- ? `${failures.length} session(s) need attention. ${failures[0]}`
202
- : 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.");
203
104
  }
204
105
  try {
205
- while (!input.signal.aborted &&
206
- store.setting("desiredState") === "running") {
207
- const connections = store.connections();
208
- store.set("heartbeatAt", new Date().toISOString());
209
- for (const connection of connections) {
210
- if (connection.state === "disconnected")
211
- continue;
212
- if (!watchers.has(connection.source.root))
213
- try {
214
- const watcher = watch(connection.source.root, { recursive: connection.source.acquisition !== "sqlite" }, () => {
215
- dirty = true;
216
- });
217
- watcher.on("error", () => {
218
- dirty = true;
219
- });
220
- watchers.set(connection.source.root, watcher);
221
- }
222
- catch {
223
- /* Reconciliation remains authoritative when platform watch is unavailable. */
224
- }
225
- }
226
- const heartbeat = Date.now() - lastHeartbeat >= COLLECTOR_DEFAULTS.heartbeatMs;
227
- 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);
228
119
  lastHeartbeat = Date.now();
229
- const reconcile = dirty ||
230
- Date.now() - lastReconcile >= COLLECTOR_DEFAULTS.reconcileMs ||
231
- store.setting("syncNow") === "yes";
232
- if (reconcile) {
233
- dirty = false;
234
- lastReconcile = Date.now();
235
- store.set("syncNow", "no");
236
- }
237
- for (let connection of connections) {
120
+ };
121
+ const check = async () => {
122
+ checkCancelled();
123
+ if (Date.now() - lastHeartbeat >= COLLECTOR_DEFAULTS.heartbeatMs)
124
+ await heartbeat();
238
125
  connection = retained(connection);
239
- if (connection.state === "disconnected")
240
- continue;
241
- let phase = "heartbeat";
242
- try {
243
- if (heartbeat) {
244
- const remote = await input.transport.heartbeat(connection, heartbeatInput(connection));
245
- connection = applyRemote(connection, remote);
246
- errors.set(connection.id, "heartbeat", null);
247
- }
248
- if (connection.state !== "active") {
249
- reconciledRevisions.delete(connection.id);
250
- continue;
251
- }
252
- phase = "source";
253
- // Resume must scan before retained one-time completion can pause again,
254
- // even when the ordinary reconcile interval has not elapsed.
255
- if (reconcile || reconciledRevisions.get(connection.id) !== connection.revision || syncRequests.needsAcquisition(connection))
256
- await acquire(connection);
257
- connection =
258
- store.connections().find((item) => item.id === connection.id) ??
259
- connection;
260
- if (connection.state !== "active")
261
- continue;
262
- if (input.signal.aborted || store.setting("desiredState") !== "running")
263
- continue;
264
- 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();
265
132
  const next = store.next(connection.id);
266
- 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";
267
144
  try {
268
- 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.
269
147
  store.acknowledge(next.entry);
270
148
  errors.set(connection.id, "admission", null);
149
+ progress({ uploaded: run.uploaded + next.entry.boundaryIds.length });
150
+ admitted = true;
151
+ break;
271
152
  }
272
153
  catch (error) {
273
- store.retry(next.entry.operationId, next.attempts);
274
- 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 });
275
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);
276
197
  connection = retained(connection);
277
- 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.");
278
201
  continue;
279
- connection = syncRequests.complete(connection, errors.message(connection.id, "heartbeat"));
280
- if (syncRequests.needsAcknowledgement(connection) && heartbeatInput(connection).completedSyncRevision) {
281
- phase = "heartbeat";
282
- const remote = await input.transport.heartbeat(connection, heartbeatInput(connection));
283
- connection = applyRemote(connection, remote);
284
- errors.set(connection.id, "heartbeat", null);
285
202
  }
286
- if (!input.signal.aborted &&
287
- store.setting("desiredState") === "running" &&
288
- connection.state === "active" &&
289
- !connection.keepSyncing &&
290
- reconciledRevisions.get(connection.id) === connection.revision &&
291
- store.queued(connection.id) === 0 &&
292
- store.progress.status(connection.id).stage === "complete" &&
293
- !errors.source(connection.id) &&
294
- syncRequests.confirmed(connection.id) >= (connection.requestedSyncRevision ?? 0)) {
295
- phase = "control";
296
- if (!input.transport.pause)
297
- throw new Error("This transport cannot acknowledge completion of a one-time import.");
298
- const paused = await input.transport.pause(connection);
299
- applyRemote(connection, paused);
300
- 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;
301
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;
302
215
  }
303
- catch (error) {
304
- 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
+ }
305
239
  }
306
240
  }
307
- await new Promise((resolve) => {
308
- const done = () => {
309
- clearTimeout(timer);
310
- input.signal.removeEventListener("abort", done);
311
- resolve();
312
- };
313
- const timer = setTimeout(done, 1000);
314
- input.signal.addEventListener("abort", done, { once: true });
315
- });
241
+ if (signal.aborted || store.setting("desiredState") !== "running")
242
+ break;
316
243
  }
317
244
  }
318
245
  finally {
319
- for (const watcher of watchers.values())
320
- watcher.close();
246
+ clearInterval(pulse);
247
+ clearTimeout(deadline);
321
248
  const owner = JSON.parse(store.setting("owner") || "{}");
322
249
  if (owner.nonce === nonce) {
323
250
  store.set("pid", "");
324
251
  store.set("heartbeatAt", "");
325
252
  store.set("owner", "");
253
+ store.set("desiredState", "stopped");
326
254
  }
327
255
  store.close();
328
256
  }
@@ -8,6 +8,7 @@ import { contentHash } from "@openpond/harness";
8
8
  import { listDatabaseSessions, readDatabaseSession } from "./database.js";
9
9
  import { claudeConversationTitle, retainedConversationTitle } from "./titles.js";
10
10
  import { NativeSessionNotReadyError } from "./readiness.js";
11
+ import { inspectCodexWindow } from "./codex-window-inventory.js";
11
12
  import { NATIVE_READ_LIMIT, } from "./contracts.js";
12
13
  async function selectedPath(source, path, boundSize = true) {
13
14
  const root = await realpath(source.root), target = await realpath(resolve(root, path));
@@ -38,6 +39,14 @@ async function readSelectedFile(source, path) {
38
39
  }
39
40
  return text;
40
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
+ }
41
50
  async function pathsBelow(source) {
42
51
  if ((await lstat(source.root)).isFile())
43
52
  return [""];
@@ -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 });