@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.
- package/dist/connected-evidence/collector-coverage-contracts.js +160 -0
- package/dist/connected-evidence/index.js +1 -0
- package/dist/execution-contracts.js +1 -1
- package/dist/javascript-isolate-node.js +2 -1
- package/dist/javascript-isolate-process-source.js +1 -1
- package/dist/javascript-isolate-process.js +2 -1
- package/dist/javascript-isolate.js +2 -1
- package/dist/javascript-verifier-worker-source.js +1 -1
- package/dist/native-conversations/codex-window-inventory.js +97 -0
- package/dist/native-conversations/collector-acquisition.js +149 -0
- package/dist/native-conversations/collector-branches.js +1 -0
- package/dist/native-conversations/collector-calendar-gate.js +19 -0
- package/dist/native-conversations/collector-contracts.js +4 -3
- package/dist/native-conversations/collector-coverage.js +100 -0
- package/dist/native-conversations/collector-cron.js +43 -0
- package/dist/native-conversations/collector-schedule.js +61 -0
- package/dist/native-conversations/collector-service-files.js +51 -0
- package/dist/native-conversations/collector-snapshots.js +70 -0
- package/dist/native-conversations/collector-source-cache.js +41 -0
- package/dist/native-conversations/collector-store.js +43 -8
- package/dist/native-conversations/collector.js +189 -261
- package/dist/native-conversations/history.js +9 -0
- package/dist/native-conversations/index.js +1 -0
- package/dist/native-conversations/location.js +4 -2
- package/dist/native-conversations/supervisor.js +131 -81
- package/dist/sql-execution-node.js +2 -1
- package/dist/tasksets.js +1 -1
- package/dist/types/connected-evidence/collector-coverage-contracts.d.ts +98 -0
- package/dist/types/connected-evidence/collector-coverage-contracts.d.ts.map +1 -0
- package/dist/types/connected-evidence/index.d.ts +1 -0
- package/dist/types/connected-evidence/index.d.ts.map +1 -1
- package/dist/types/javascript-isolate-node.d.ts.map +1 -1
- package/dist/types/javascript-isolate-process.d.ts.map +1 -1
- package/dist/types/javascript-isolate.d.ts.map +1 -1
- package/dist/types/native-conversations/codex-window-inventory.d.ts +12 -0
- package/dist/types/native-conversations/codex-window-inventory.d.ts.map +1 -0
- package/dist/types/native-conversations/collector-acquisition.d.ts +17 -0
- package/dist/types/native-conversations/collector-acquisition.d.ts.map +1 -0
- package/dist/types/native-conversations/collector-branches.d.ts.map +1 -1
- package/dist/types/native-conversations/collector-calendar-gate.d.ts +9 -0
- package/dist/types/native-conversations/collector-calendar-gate.d.ts.map +1 -0
- package/dist/types/native-conversations/collector-contracts.d.ts +41 -5
- package/dist/types/native-conversations/collector-contracts.d.ts.map +1 -1
- package/dist/types/native-conversations/collector-coverage.d.ts +62 -0
- package/dist/types/native-conversations/collector-coverage.d.ts.map +1 -0
- package/dist/types/native-conversations/collector-cron.d.ts +20 -0
- package/dist/types/native-conversations/collector-cron.d.ts.map +1 -0
- package/dist/types/native-conversations/collector-schedule.d.ts +13 -0
- package/dist/types/native-conversations/collector-schedule.d.ts.map +1 -0
- package/dist/types/native-conversations/collector-service-files.d.ts +20 -0
- package/dist/types/native-conversations/collector-service-files.d.ts.map +1 -0
- package/dist/types/native-conversations/collector-snapshots.d.ts +25 -0
- package/dist/types/native-conversations/collector-snapshots.d.ts.map +1 -0
- package/dist/types/native-conversations/collector-source-cache.d.ts +31 -0
- package/dist/types/native-conversations/collector-source-cache.d.ts.map +1 -0
- package/dist/types/native-conversations/collector-store.d.ts +11 -2
- package/dist/types/native-conversations/collector-store.d.ts.map +1 -1
- package/dist/types/native-conversations/collector.d.ts +5 -1
- package/dist/types/native-conversations/collector.d.ts.map +1 -1
- package/dist/types/native-conversations/history.d.ts +7 -0
- package/dist/types/native-conversations/history.d.ts.map +1 -1
- package/dist/types/native-conversations/index.d.ts +1 -0
- package/dist/types/native-conversations/index.d.ts.map +1 -1
- package/dist/types/native-conversations/location.d.ts.map +1 -1
- package/dist/types/native-conversations/supervisor.d.ts +4 -3
- package/dist/types/native-conversations/supervisor.d.ts.map +1 -1
- package/dist/types/sql-execution-node.d.ts.map +1 -1
- package/package.json +3 -2
- 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 {
|
|
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 {
|
|
9
|
-
import {
|
|
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 {
|
|
12
|
-
|
|
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
|
|
18
|
-
if (
|
|
19
|
-
const owner = JSON.parse(
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
304
|
-
|
|
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
|
-
|
|
308
|
-
|
|
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
|
-
|
|
320
|
-
|
|
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 [""];
|
|
@@ -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 =
|
|
6
|
-
return
|
|
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 });
|