@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.
- package/dist/connected-evidence/collector-coverage-contracts.js +160 -0
- package/dist/connected-evidence/contracts.js +1 -1
- package/dist/connected-evidence/imports.js +2 -1
- package/dist/connected-evidence/index.js +1 -0
- package/dist/connected-evidence/normalize.js +14 -4
- 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 +47 -8
- package/dist/native-conversations/collector.js +189 -254
- package/dist/native-conversations/history.js +44 -6
- package/dist/native-conversations/index.js +1 -0
- package/dist/native-conversations/location.js +4 -2
- package/dist/native-conversations/readiness.js +4 -0
- package/dist/native-conversations/supervisor.js +131 -81
- package/dist/native-conversations/titles.js +38 -0
- 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/contracts.d.ts.map +1 -1
- package/dist/types/connected-evidence/imports.d.ts +1 -0
- package/dist/types/connected-evidence/imports.d.ts.map +1 -1
- package/dist/types/connected-evidence/index.d.ts +1 -0
- package/dist/types/connected-evidence/index.d.ts.map +1 -1
- package/dist/types/connected-evidence/normalize.d.ts +3 -0
- package/dist/types/connected-evidence/normalize.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 +45 -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/readiness.d.ts +5 -0
- package/dist/types/native-conversations/readiness.d.ts.map +1 -0
- package/dist/types/native-conversations/supervisor.d.ts +4 -3
- package/dist/types/native-conversations/supervisor.d.ts.map +1 -1
- package/dist/types/native-conversations/titles.d.ts +4 -0
- package/dist/types/native-conversations/titles.d.ts.map +1 -0
- package/dist/types/sql-execution-node.d.ts.map +1 -1
- package/package.json +4 -3
- 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 {
|
|
2
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
6
3
|
import { CollectorStore } from "./collector-store.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
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 {
|
|
11
|
-
|
|
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
|
|
17
|
-
if (
|
|
18
|
-
const owner = JSON.parse(
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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
|
-
|
|
301
|
-
|
|
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
|
-
|
|
313
|
-
|
|
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 =
|
|
155
|
-
?
|
|
156
|
-
: typeof payload.
|
|
157
|
-
? payload.
|
|
158
|
-
:
|
|
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 =
|
|
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" &&
|
|
@@ -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 });
|