@phigon/observer 8.2.0 → 8.3.0
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/README.md +194 -11
- package/dist/bin/guardian-runner-darwin-amd64 +0 -0
- package/dist/bin/guardian-runner-darwin-arm64 +0 -0
- package/dist/bin/guardian-runner-linux-amd64 +0 -0
- package/dist/bin/guardian-runner-linux-arm64 +0 -0
- package/dist/bin/guardian-runner-windows-amd64.exe +0 -0
- package/dist/bin/guardian-runner-windows-arm64.exe +0 -0
- package/dist/src/abortable-delay.d.ts +10 -0
- package/dist/src/abortable-delay.d.ts.map +1 -0
- package/dist/src/abortable-delay.js +21 -0
- package/dist/src/abortable-delay.js.map +1 -0
- package/dist/src/activity/adapters/claude.d.ts.map +1 -1
- package/dist/src/activity/adapters/claude.js +9 -1
- package/dist/src/activity/adapters/claude.js.map +1 -1
- package/dist/src/activity/adapters/openai-compatible.d.ts.map +1 -1
- package/dist/src/activity/adapters/openai-compatible.js +9 -1
- package/dist/src/activity/adapters/openai-compatible.js.map +1 -1
- package/dist/src/activity/collector.d.ts +31 -1
- package/dist/src/activity/collector.d.ts.map +1 -1
- package/dist/src/activity/collector.js +337 -77
- package/dist/src/activity/collector.js.map +1 -1
- package/dist/src/activity/ingest.d.ts +9 -0
- package/dist/src/activity/ingest.d.ts.map +1 -1
- package/dist/src/activity/ingest.js +92 -3
- package/dist/src/activity/ingest.js.map +1 -1
- package/dist/src/activity/json-document-source.d.ts +15 -0
- package/dist/src/activity/json-document-source.d.ts.map +1 -1
- package/dist/src/activity/json-document-source.js +19 -2
- package/dist/src/activity/json-document-source.js.map +1 -1
- package/dist/src/activity/native-capabilities.d.ts.map +1 -1
- package/dist/src/activity/native-capabilities.js +8 -12
- package/dist/src/activity/native-capabilities.js.map +1 -1
- package/dist/src/activity/opencode-sqlite-source.js +1 -0
- package/dist/src/activity/opencode-sqlite-source.js.map +1 -1
- package/dist/src/activity/source-reader.d.ts +44 -3
- package/dist/src/activity/source-reader.d.ts.map +1 -1
- package/dist/src/activity/source-reader.js +325 -86
- package/dist/src/activity/source-reader.js.map +1 -1
- package/dist/src/agent-action-executor.d.ts +5 -0
- package/dist/src/agent-action-executor.d.ts.map +1 -1
- package/dist/src/agent-action-executor.js +9 -6
- package/dist/src/agent-action-executor.js.map +1 -1
- package/dist/src/atomic-replace.d.ts +7 -0
- package/dist/src/atomic-replace.d.ts.map +1 -1
- package/dist/src/atomic-replace.js +28 -3
- package/dist/src/atomic-replace.js.map +1 -1
- package/dist/src/bridge-journal.d.ts +32 -0
- package/dist/src/bridge-journal.d.ts.map +1 -0
- package/dist/src/bridge-journal.js +113 -0
- package/dist/src/bridge-journal.js.map +1 -0
- package/dist/src/cli.d.ts +7 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +490 -43
- package/dist/src/cli.js.map +1 -1
- package/dist/src/collection-notice.d.ts +34 -0
- package/dist/src/collection-notice.d.ts.map +1 -0
- package/dist/src/collection-notice.js +73 -0
- package/dist/src/collection-notice.js.map +1 -0
- package/dist/src/config.d.ts +26 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +88 -33
- package/dist/src/config.js.map +1 -1
- package/dist/src/content-redaction.d.ts +17 -1
- package/dist/src/content-redaction.d.ts.map +1 -1
- package/dist/src/content-redaction.js +44 -3
- package/dist/src/content-redaction.js.map +1 -1
- package/dist/src/credential-store.d.ts +20 -1
- package/dist/src/credential-store.d.ts.map +1 -1
- package/dist/src/credential-store.js +27 -3
- package/dist/src/credential-store.js.map +1 -1
- package/dist/src/discovery/engine.d.ts.map +1 -1
- package/dist/src/discovery/engine.js +1 -11
- package/dist/src/discovery/engine.js.map +1 -1
- package/dist/src/doctor-check.d.ts +51 -0
- package/dist/src/doctor-check.d.ts.map +1 -0
- package/dist/src/doctor-check.js +174 -0
- package/dist/src/doctor-check.js.map +1 -0
- package/dist/src/enroll.d.ts +86 -0
- package/dist/src/enroll.d.ts.map +1 -0
- package/dist/src/enroll.js +245 -0
- package/dist/src/enroll.js.map +1 -0
- package/dist/src/entry.d.ts +3 -0
- package/dist/src/entry.d.ts.map +1 -0
- package/dist/src/entry.js +14 -0
- package/dist/src/entry.js.map +1 -0
- package/dist/src/event-queue.d.ts +13 -1
- package/dist/src/event-queue.d.ts.map +1 -1
- package/dist/src/event-queue.js +50 -5
- package/dist/src/event-queue.js.map +1 -1
- package/dist/src/event-spool.d.ts +61 -3
- package/dist/src/event-spool.d.ts.map +1 -1
- package/dist/src/event-spool.js +290 -41
- package/dist/src/event-spool.js.map +1 -1
- package/dist/src/generated/phigon/v1/identity_pb.d.ts +7 -0
- package/dist/src/generated/phigon/v1/identity_pb.d.ts.map +1 -1
- package/dist/src/generated/phigon/v1/identity_pb.js +1 -1
- package/dist/src/generated/phigon/v1/identity_pb.js.map +1 -1
- package/dist/src/guardian-decision-journal.d.ts.map +1 -1
- package/dist/src/guardian-decision-journal.js +4 -20
- package/dist/src/guardian-decision-journal.js.map +1 -1
- package/dist/src/guardian-managed-exec.js +6 -19
- package/dist/src/guardian-managed-exec.js.map +1 -1
- package/dist/src/guardian-process-monitor.d.ts +1 -0
- package/dist/src/guardian-process-monitor.d.ts.map +1 -1
- package/dist/src/guardian-process-monitor.js +74 -26
- package/dist/src/guardian-process-monitor.js.map +1 -1
- package/dist/src/health.d.ts +19 -1
- package/dist/src/health.d.ts.map +1 -1
- package/dist/src/health.js +22 -1
- package/dist/src/health.js.map +1 -1
- package/dist/src/historical-identity-store.d.ts +8 -1
- package/dist/src/historical-identity-store.d.ts.map +1 -1
- package/dist/src/historical-identity-store.js +26 -9
- package/dist/src/historical-identity-store.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/installation.d.ts +2 -0
- package/dist/src/installation.d.ts.map +1 -1
- package/dist/src/installation.js +3 -1
- package/dist/src/installation.js.map +1 -1
- package/dist/src/instance-lock.d.ts +45 -0
- package/dist/src/instance-lock.d.ts.map +1 -0
- package/dist/src/instance-lock.js +243 -0
- package/dist/src/instance-lock.js.map +1 -0
- package/dist/src/machine-command-executor.d.ts +2 -2
- package/dist/src/machine-command-executor.d.ts.map +1 -1
- package/dist/src/machine-command-executor.js +7 -20
- package/dist/src/machine-command-executor.js.map +1 -1
- package/dist/src/node-version.d.ts +8 -0
- package/dist/src/node-version.d.ts.map +1 -0
- package/dist/src/node-version.js +17 -0
- package/dist/src/node-version.js.map +1 -0
- package/dist/src/observer-log.d.ts +50 -0
- package/dist/src/observer-log.d.ts.map +1 -0
- package/dist/src/observer-log.js +141 -0
- package/dist/src/observer-log.js.map +1 -0
- package/dist/src/pair.d.ts +68 -1
- package/dist/src/pair.d.ts.map +1 -1
- package/dist/src/pair.js +204 -66
- package/dist/src/pair.js.map +1 -1
- package/dist/src/private-directory.d.ts +6 -0
- package/dist/src/private-directory.d.ts.map +1 -1
- package/dist/src/private-directory.js +33 -2
- package/dist/src/private-directory.js.map +1 -1
- package/dist/src/proxy.d.ts +68 -0
- package/dist/src/proxy.d.ts.map +1 -0
- package/dist/src/proxy.js +209 -0
- package/dist/src/proxy.js.map +1 -0
- package/dist/src/purge.d.ts +17 -0
- package/dist/src/purge.d.ts.map +1 -0
- package/dist/src/purge.js +84 -0
- package/dist/src/purge.js.map +1 -0
- package/dist/src/remove-machine.d.ts +5 -1
- package/dist/src/remove-machine.d.ts.map +1 -1
- package/dist/src/remove-machine.js +28 -14
- package/dist/src/remove-machine.js.map +1 -1
- package/dist/src/server-authority.d.ts +5 -2
- package/dist/src/server-authority.d.ts.map +1 -1
- package/dist/src/server-authority.js +10 -6
- package/dist/src/server-authority.js.map +1 -1
- package/dist/src/server-clock.d.ts +32 -0
- package/dist/src/server-clock.d.ts.map +1 -0
- package/dist/src/server-clock.js +72 -0
- package/dist/src/server-clock.js.map +1 -0
- package/dist/src/service-command.d.ts +20 -0
- package/dist/src/service-command.d.ts.map +1 -0
- package/dist/src/service-command.js +136 -0
- package/dist/src/service-command.js.map +1 -0
- package/dist/src/service.d.ts +98 -0
- package/dist/src/service.d.ts.map +1 -0
- package/dist/src/service.js +472 -0
- package/dist/src/service.js.map +1 -0
- package/dist/src/stream-client.d.ts +18 -0
- package/dist/src/stream-client.d.ts.map +1 -1
- package/dist/src/stream-client.js +91 -16
- package/dist/src/stream-client.js.map +1 -1
- package/dist/src/stream-state.d.ts +2 -0
- package/dist/src/stream-state.d.ts.map +1 -1
- package/dist/src/stream-state.js +3 -1
- package/dist/src/stream-state.js.map +1 -1
- package/dist/src/user-notices.d.ts +11 -0
- package/dist/src/user-notices.d.ts.map +1 -1
- package/dist/src/user-notices.js +24 -0
- package/dist/src/user-notices.js.map +1 -1
- package/package.json +7 -7
|
@@ -4,7 +4,7 @@ import { chmod, mkdir, open, readFile, readdir, realpath, stat, unlink, writeFil
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { basename, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { atomicReplace } from "../atomic-replace.js";
|
|
7
|
-
import { DirectoryListingCache, scanJSONDocumentSource, } from "./json-document-source.js";
|
|
7
|
+
import { DirectoryListingCache, scanJSONDocumentSource, startedAfter, } from "./json-document-source.js";
|
|
8
8
|
import { SQLiteConnections } from "./sqlite-connections.js";
|
|
9
9
|
const maxSourceFiles = 256;
|
|
10
10
|
const historicalFilesPerScan = 16;
|
|
@@ -17,6 +17,12 @@ const defaultInitialReplayBytes = 256 << 10;
|
|
|
17
17
|
const readWindowBytes = 1 << 20;
|
|
18
18
|
const maxLineBytes = 6 << 20;
|
|
19
19
|
const maxCursorEntries = 16_384;
|
|
20
|
+
// A source or file that fails is skipped for a while, twice as long after
|
|
21
|
+
// each further failure, so one broken agent folder or database cannot stop
|
|
22
|
+
// the others or be retried on every scan.
|
|
23
|
+
const failureRetryBaseMs = 5_000;
|
|
24
|
+
const failureRetryMaxMs = 5 * 60_000;
|
|
25
|
+
const maxTrackedFailures = 256;
|
|
20
26
|
/**
|
|
21
27
|
* Source cursors in least-recently-used order, so a full store evicts the
|
|
22
28
|
* cursors of sessions nobody reads any more instead of live ones (an
|
|
@@ -26,6 +32,8 @@ const maxCursorEntries = 16_384;
|
|
|
26
32
|
*/
|
|
27
33
|
class RecentCursors extends Map {
|
|
28
34
|
#used = new Set();
|
|
35
|
+
// Values from before `track`, for each cursor changed since.
|
|
36
|
+
#journal;
|
|
29
37
|
get(key) {
|
|
30
38
|
const value = super.get(key);
|
|
31
39
|
if (value !== undefined)
|
|
@@ -33,9 +41,36 @@ class RecentCursors extends Map {
|
|
|
33
41
|
return value;
|
|
34
42
|
}
|
|
35
43
|
set(key, value) {
|
|
44
|
+
this.#remember(key);
|
|
36
45
|
super.delete(key);
|
|
37
46
|
return super.set(key, value);
|
|
38
47
|
}
|
|
48
|
+
delete(key) {
|
|
49
|
+
this.#remember(key);
|
|
50
|
+
return super.delete(key);
|
|
51
|
+
}
|
|
52
|
+
/** Start remembering the cursors a source changes. */
|
|
53
|
+
track() {
|
|
54
|
+
this.#journal = new Map();
|
|
55
|
+
}
|
|
56
|
+
/** Stop remembering; with `rollback`, undo every change since `track`. */
|
|
57
|
+
untrack(rollback) {
|
|
58
|
+
const journal = this.#journal;
|
|
59
|
+
this.#journal = undefined;
|
|
60
|
+
if (!rollback || journal === undefined)
|
|
61
|
+
return;
|
|
62
|
+
for (const [key, value] of journal) {
|
|
63
|
+
if (value === undefined)
|
|
64
|
+
super.delete(key);
|
|
65
|
+
else
|
|
66
|
+
super.set(key, value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
#remember(key) {
|
|
70
|
+
if (this.#journal !== undefined && !this.#journal.has(key)) {
|
|
71
|
+
this.#journal.set(key, super.get(key));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
39
74
|
/** Move cursors read since the last call to the recent end, then bound. */
|
|
40
75
|
settle(limit) {
|
|
41
76
|
for (const key of this.#used) {
|
|
@@ -67,7 +102,7 @@ export class ActivityCursorStore {
|
|
|
67
102
|
}
|
|
68
103
|
catch (error) {
|
|
69
104
|
if (isNodeError(error) && error.code === "ENOENT")
|
|
70
|
-
return new Map();
|
|
105
|
+
return { cursors: new Map() };
|
|
71
106
|
throw error;
|
|
72
107
|
}
|
|
73
108
|
let value;
|
|
@@ -84,7 +119,7 @@ export class ActivityCursorStore {
|
|
|
84
119
|
const recovered = new Map();
|
|
85
120
|
await this.save(recovered);
|
|
86
121
|
process.emitWarning("Saved activity checkpoints were unreadable. Available source history will be rescanned using stable record IDs.", { code: "PHIGON_CURSOR_RECOVERY" });
|
|
87
|
-
return recovered;
|
|
122
|
+
return { cursors: recovered };
|
|
88
123
|
}
|
|
89
124
|
if (typeof value !== "object" ||
|
|
90
125
|
value === null ||
|
|
@@ -106,15 +141,24 @@ export class ActivityCursorStore {
|
|
|
106
141
|
result.set(key, offset);
|
|
107
142
|
}
|
|
108
143
|
}
|
|
109
|
-
|
|
144
|
+
const scannedAt = "scanned_at" in value && typeof value.scanned_at === "string"
|
|
145
|
+
? Date.parse(value.scanned_at)
|
|
146
|
+
: Number.NaN;
|
|
147
|
+
return Number.isFinite(scannedAt)
|
|
148
|
+
? { cursors: result, scannedAt }
|
|
149
|
+
: { cursors: result };
|
|
110
150
|
}
|
|
111
|
-
async save(cursors) {
|
|
151
|
+
async save(cursors, scannedAt) {
|
|
112
152
|
await mkdir(this.#dataDirectory, { recursive: true, mode: 0o700 });
|
|
113
153
|
await makeDirectoryPrivate(this.#dataDirectory);
|
|
114
154
|
// Keep the map's order (least recently used first) so a restarted
|
|
115
155
|
// reader still knows which cursors are stale.
|
|
116
156
|
const files = Object.fromEntries([...cursors.entries()].slice(-maxCursorEntries));
|
|
117
|
-
const state = {
|
|
157
|
+
const state = {
|
|
158
|
+
version: 1,
|
|
159
|
+
files,
|
|
160
|
+
...(scannedAt === undefined ? {} : { scanned_at: new Date(scannedAt).toISOString() }),
|
|
161
|
+
};
|
|
118
162
|
const temporary = `${this.#path}.${process.pid}.${randomUUID()}.tmp`;
|
|
119
163
|
try {
|
|
120
164
|
await writeFile(temporary, `${JSON.stringify(state)}\n`, {
|
|
@@ -148,6 +192,14 @@ export class JSONLSourceReader {
|
|
|
148
192
|
// identical checkpoint every tick; rewriting and flushing it would cost a
|
|
149
193
|
// synchronous disk write per scan interval without changing anything.
|
|
150
194
|
#savedCursors;
|
|
195
|
+
#savedScannedAt;
|
|
196
|
+
// When the scan behind the saved cursors started, as loaded at startup.
|
|
197
|
+
// Undefined on the first start ever. A file without a cursor that began
|
|
198
|
+
// after it was created while the observer was stopped.
|
|
199
|
+
#lastRunAt;
|
|
200
|
+
// The cursors as the previous scan left them, which is where the next
|
|
201
|
+
// scan starts. Kept so a position costs no copy on every scan.
|
|
202
|
+
#scanStart;
|
|
151
203
|
#initialScanComplete = false;
|
|
152
204
|
#generation = 0;
|
|
153
205
|
#committedGeneration = 0;
|
|
@@ -167,26 +219,40 @@ export class JSONLSourceReader {
|
|
|
167
219
|
#documentListings = new DirectoryListingCache();
|
|
168
220
|
#gooseConnections = new SQLiteConnections();
|
|
169
221
|
#zedConnections = new SQLiteConnections();
|
|
170
|
-
|
|
222
|
+
#failures = new Map();
|
|
223
|
+
// Live sources that were skipped or failed while their first scan would
|
|
224
|
+
// have baselined them. Their first successful scan baselines instead, so
|
|
225
|
+
// an old archive is not read as new work once the source recovers.
|
|
226
|
+
#unbaselinedSources = new Set();
|
|
227
|
+
#clock;
|
|
228
|
+
constructor(dataDirectory, replayOnFirstStart = false, initialReplayBytes = defaultInitialReplayBytes, initialReplaySessions = 1, mode = "live", clock = Date.now) {
|
|
171
229
|
this.#cursorStore = new ActivityCursorStore(dataDirectory);
|
|
172
230
|
this.#replayOnFirstStart = replayOnFirstStart;
|
|
173
231
|
this.#initialReplayBytes = Math.min(maxReadBytesPerFile, Math.max(1, initialReplayBytes));
|
|
174
232
|
this.#initialReplaySessions = Math.min(maxSourceFiles, Math.max(1, initialReplaySessions));
|
|
175
233
|
this.#mode = mode;
|
|
234
|
+
this.#clock = clock;
|
|
176
235
|
}
|
|
177
236
|
async scan(sources, maxRecords) {
|
|
178
237
|
if (this.#cursors === undefined) {
|
|
179
238
|
const loaded = await this.#cursorStore.load();
|
|
180
239
|
this.#cursors = new RecentCursors();
|
|
181
|
-
for (const [key, offset] of loaded)
|
|
240
|
+
for (const [key, offset] of loaded.cursors)
|
|
182
241
|
this.#cursors.set(key, offset);
|
|
183
|
-
this.#savedCursors = loaded;
|
|
242
|
+
this.#savedCursors = loaded.cursors;
|
|
243
|
+
this.#savedScannedAt = loaded.scannedAt;
|
|
244
|
+
this.#lastRunAt = loaded.scannedAt;
|
|
245
|
+
this.#scanStart = loaded.cursors;
|
|
184
246
|
}
|
|
247
|
+
const scannedAt = this.#clock();
|
|
185
248
|
const records = [];
|
|
186
249
|
let fileCount = 0;
|
|
187
250
|
let malformed = 0;
|
|
188
251
|
let oversized = 0;
|
|
189
252
|
let candidates = [];
|
|
253
|
+
const initialScan = !this.#initialScanComplete;
|
|
254
|
+
// Files of sources that missed their baseline, baselined on this scan.
|
|
255
|
+
const baselinePaths = new Set();
|
|
190
256
|
const cacheKey = JSON.stringify(sources);
|
|
191
257
|
if (this.#mode === "history" && this.#sourceCache?.key === cacheKey && Date.now() - this.#sourceCache.at < 5_000) {
|
|
192
258
|
candidates = this.#sourceCache.files;
|
|
@@ -196,56 +262,19 @@ export class JSONLSourceReader {
|
|
|
196
262
|
for (const source of sources) {
|
|
197
263
|
if (source.format !== undefined && source.format !== "jsonl")
|
|
198
264
|
continue;
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
try {
|
|
213
|
-
// The listing never follows links, so a listed file keeps its
|
|
214
|
-
// canonical path while its root resolves to the same place and
|
|
215
|
-
// the file keeps its identity. Resolve each file only once.
|
|
216
|
-
rootReal ??= await realpath(root);
|
|
217
|
-
const key = `${rootReal}\u0000${file.path}`;
|
|
218
|
-
const identity = fileIdentityKey(file.identity);
|
|
219
|
-
const known = this.#canonicalPaths.get(key);
|
|
220
|
-
canonicalPath =
|
|
221
|
-
known !== undefined && identity !== undefined &&
|
|
222
|
-
known.identity === identity
|
|
223
|
-
? known.path
|
|
224
|
-
: await realpath(file.path);
|
|
225
|
-
if (identity !== undefined) {
|
|
226
|
-
canonicalPaths.set(key, { path: canonicalPath, identity });
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
catch (error) {
|
|
230
|
-
if (isTransientSourceError(error))
|
|
231
|
-
continue;
|
|
232
|
-
throw error;
|
|
233
|
-
}
|
|
234
|
-
candidates.push({
|
|
235
|
-
adapterId: source.adapterId,
|
|
236
|
-
...(source.sourceDisplayName === undefined
|
|
237
|
-
? {}
|
|
238
|
-
: { sourceDisplayName: source.sourceDisplayName }),
|
|
239
|
-
root,
|
|
240
|
-
path: canonicalPath,
|
|
241
|
-
modifiedMs: file.modifiedMs,
|
|
242
|
-
identity: file.identity,
|
|
243
|
-
});
|
|
244
|
-
if (candidates.length > maxCursorEntries * 2) {
|
|
245
|
-
candidates = reconciledSourceFiles(candidates);
|
|
246
|
-
candidates.sort(recentFileFirst);
|
|
247
|
-
candidates.length = maxCursorEntries;
|
|
248
|
-
}
|
|
265
|
+
const key = sourceKey(source);
|
|
266
|
+
const sourceInitial = initialScan || this.#unbaselinedSources.has(key);
|
|
267
|
+
const listed = await this.#isolated(key, source.adapterId, sourceInitial, () => this.#listJSONLSource(source, canonicalPaths));
|
|
268
|
+
if (listed === undefined)
|
|
269
|
+
continue;
|
|
270
|
+
for (const file of listed) {
|
|
271
|
+
candidates.push(file);
|
|
272
|
+
if (sourceInitial && !initialScan)
|
|
273
|
+
baselinePaths.add(file.path);
|
|
274
|
+
if (candidates.length > maxCursorEntries * 2) {
|
|
275
|
+
candidates = reconciledSourceFiles(candidates);
|
|
276
|
+
candidates.sort(recentFileFirst);
|
|
277
|
+
candidates.length = maxCursorEntries;
|
|
249
278
|
}
|
|
250
279
|
}
|
|
251
280
|
}
|
|
@@ -255,22 +284,24 @@ export class JSONLSourceReader {
|
|
|
255
284
|
const recentCandidates = reconciledSourceFiles(candidates)
|
|
256
285
|
.sort(recentFileFirst)
|
|
257
286
|
.slice(0, maxCursorEntries);
|
|
258
|
-
const initialScan = !this.#initialScanComplete;
|
|
259
287
|
const selected = this.#mode === "history"
|
|
260
288
|
? await this.#pendingHistoryFiles(recentCandidates)
|
|
261
289
|
: recentCandidates.slice(0, maxSourceFiles);
|
|
262
290
|
const replayFiles = initialScan && this.#replayOnFirstStart
|
|
263
291
|
? newestFilesPerAdapter(selected, this.#initialReplaySessions)
|
|
264
292
|
: new Set();
|
|
265
|
-
if (
|
|
293
|
+
if (this.#mode === "live" && (initialScan || baselinePaths.size > 0)) {
|
|
266
294
|
for (const file of [...recentCandidates].reverse()) {
|
|
295
|
+
if (!initialScan && !baselinePaths.has(file.path))
|
|
296
|
+
continue;
|
|
267
297
|
if (!replayFiles.has(file.path)) {
|
|
268
298
|
try {
|
|
269
299
|
await this.#baselineFile(file.root, file.path);
|
|
270
300
|
}
|
|
271
301
|
catch (error) {
|
|
272
|
-
if (!isTransientSourceError(error))
|
|
273
|
-
|
|
302
|
+
if (!isTransientSourceError(error)) {
|
|
303
|
+
this.#recordFailure(fileKey(file.path), file.adapterId, error);
|
|
304
|
+
}
|
|
274
305
|
}
|
|
275
306
|
}
|
|
276
307
|
}
|
|
@@ -290,6 +321,8 @@ export class JSONLSourceReader {
|
|
|
290
321
|
fileCount++;
|
|
291
322
|
continue;
|
|
292
323
|
}
|
|
324
|
+
if (this.#skipping(fileKey(file.path)))
|
|
325
|
+
continue;
|
|
293
326
|
let result;
|
|
294
327
|
try {
|
|
295
328
|
const rootReal = rootRealPaths.get(file.root) ??
|
|
@@ -298,10 +331,12 @@ export class JSONLSourceReader {
|
|
|
298
331
|
result = await this.#readFile(file.adapterId, rootReal, file.path, replayFiles.has(file.path), file.sourceDisplayName, remainingRecords - reservedDatabaseRecords);
|
|
299
332
|
}
|
|
300
333
|
catch (error) {
|
|
301
|
-
if (isTransientSourceError(error))
|
|
302
|
-
|
|
303
|
-
|
|
334
|
+
if (!isTransientSourceError(error)) {
|
|
335
|
+
this.#recordFailure(fileKey(file.path), file.adapterId, error);
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
304
338
|
}
|
|
339
|
+
this.#failures.delete(fileKey(file.path));
|
|
305
340
|
fileCount++;
|
|
306
341
|
records.push(...result.records);
|
|
307
342
|
malformed += result.malformed;
|
|
@@ -315,25 +350,31 @@ export class JSONLSourceReader {
|
|
|
315
350
|
}
|
|
316
351
|
let documentsComplete = true;
|
|
317
352
|
let documentFiles = 0;
|
|
353
|
+
const cursors = this.#cursors;
|
|
318
354
|
for (const source of sources) {
|
|
319
355
|
if (source.format !== "json-messages")
|
|
320
356
|
continue;
|
|
321
|
-
const
|
|
357
|
+
const key = sourceKey(source);
|
|
358
|
+
const sourceInitial = initialScan || this.#unbaselinedSources.has(key);
|
|
359
|
+
const documents = await this.#isolated(key, source.adapterId, sourceInitial, () => scanJSONDocumentSource({
|
|
322
360
|
adapterId: source.adapterId,
|
|
323
361
|
roots: source.roots.map(expandedActivityRoot),
|
|
324
362
|
pathPattern: source.pathPattern ?? "\\.json$",
|
|
325
363
|
messagesField: source.messagesField ?? "messages",
|
|
326
364
|
...source.document,
|
|
327
365
|
}, {
|
|
328
|
-
cursors
|
|
366
|
+
cursors,
|
|
329
367
|
listings: this.#documentListings,
|
|
330
|
-
initialScan,
|
|
368
|
+
initialScan: sourceInitial,
|
|
331
369
|
replayOnFirstStart: this.#replayOnFirstStart,
|
|
332
370
|
replaySessions: this.#initialReplaySessions,
|
|
333
371
|
maxReplayRecords: Math.max(32, Math.floor(this.#initialReplayBytes / 1_024)),
|
|
334
372
|
maxRecords: Math.max(0, remainingRecords - reservedDatabaseRecords),
|
|
335
373
|
historical: this.#mode === "history",
|
|
336
|
-
|
|
374
|
+
...(this.#lastRunAt === undefined ? {} : { newSince: this.#lastRunAt }),
|
|
375
|
+
}));
|
|
376
|
+
if (documents === undefined)
|
|
377
|
+
continue;
|
|
337
378
|
records.push(...documents.records);
|
|
338
379
|
fileCount += documents.files;
|
|
339
380
|
malformed += documents.malformed;
|
|
@@ -351,17 +392,21 @@ export class JSONLSourceReader {
|
|
|
351
392
|
}
|
|
352
393
|
// Loaded on demand for the same reason as the OpenCode source below.
|
|
353
394
|
const { scanGooseSQLiteSources } = await import("./goose-sqlite-source.js");
|
|
354
|
-
const
|
|
395
|
+
const key = sourceKey(source);
|
|
396
|
+
const sourceInitial = initialScan || this.#unbaselinedSources.has(key);
|
|
397
|
+
const goose = await this.#isolated(key, source.adapterId, sourceInitial, () => scanGooseSQLiteSources({
|
|
355
398
|
adapterId: source.adapterId,
|
|
356
399
|
roots: source.roots.map(expandedActivityRoot),
|
|
357
|
-
cursors
|
|
358
|
-
initialScan,
|
|
400
|
+
cursors,
|
|
401
|
+
initialScan: sourceInitial,
|
|
359
402
|
replayOnFirstStart: this.#replayOnFirstStart,
|
|
360
403
|
maxReplayRecords: Math.max(32, Math.floor(this.#initialReplayBytes / 1_024)),
|
|
361
404
|
maxRecords: remainingRecords - reservedDatabaseRecords,
|
|
362
405
|
historical: this.#mode === "history",
|
|
363
406
|
...(this.#mode === "live" ? { connections: this.#gooseConnections } : { completedHistory: this.#completedHistoryDatabases }),
|
|
364
|
-
});
|
|
407
|
+
}));
|
|
408
|
+
if (goose === undefined)
|
|
409
|
+
continue;
|
|
365
410
|
records.push(...goose.records);
|
|
366
411
|
fileCount += goose.files;
|
|
367
412
|
malformed += goose.malformed;
|
|
@@ -378,18 +423,22 @@ export class JSONLSourceReader {
|
|
|
378
423
|
break;
|
|
379
424
|
}
|
|
380
425
|
const { scanZedSQLiteSources } = await import("./zed-sqlite-source.js");
|
|
381
|
-
const
|
|
426
|
+
const key = sourceKey(source);
|
|
427
|
+
const sourceInitial = initialScan || this.#unbaselinedSources.has(key);
|
|
428
|
+
const zed = await this.#isolated(key, source.adapterId, sourceInitial, () => scanZedSQLiteSources({
|
|
382
429
|
adapterId: source.adapterId,
|
|
383
430
|
roots: source.roots.map(expandedActivityRoot),
|
|
384
|
-
cursors
|
|
385
|
-
initialScan,
|
|
431
|
+
cursors,
|
|
432
|
+
initialScan: sourceInitial,
|
|
386
433
|
replayOnFirstStart: this.#replayOnFirstStart,
|
|
387
434
|
replaySessions: this.#initialReplaySessions,
|
|
388
435
|
maxReplayRecords: Math.max(32, Math.floor(this.#initialReplayBytes / 1_024)),
|
|
389
436
|
maxRecords: remainingRecords - reservedDatabaseRecords,
|
|
390
437
|
historical: this.#mode === "history",
|
|
391
438
|
...(this.#mode === "live" ? { connections: this.#zedConnections } : { completedHistory: this.#completedHistoryDatabases }),
|
|
392
|
-
});
|
|
439
|
+
}));
|
|
440
|
+
if (zed === undefined)
|
|
441
|
+
continue;
|
|
393
442
|
records.push(...zed.records);
|
|
394
443
|
fileCount += zed.files;
|
|
395
444
|
malformed += zed.malformed;
|
|
@@ -411,16 +460,20 @@ export class JSONLSourceReader {
|
|
|
411
460
|
// when an OpenCode data source is actually scanned so commands such as
|
|
412
461
|
// pair, doctor, and help stay warning-free.
|
|
413
462
|
const { scanOpenCodeSQLiteSources } = await import("./opencode-sqlite-source.js");
|
|
414
|
-
const
|
|
463
|
+
const key = sourceKey(source);
|
|
464
|
+
const sourceInitial = initialScan || this.#unbaselinedSources.has(key);
|
|
465
|
+
const sqlite = await this.#isolated(key, source.adapterId, sourceInitial, () => scanOpenCodeSQLiteSources({
|
|
415
466
|
adapterId: source.adapterId,
|
|
416
467
|
roots: source.roots.map(expandedActivityRoot),
|
|
417
|
-
cursors
|
|
418
|
-
initialScan,
|
|
468
|
+
cursors,
|
|
469
|
+
initialScan: sourceInitial,
|
|
419
470
|
replayOnFirstStart: this.#replayOnFirstStart,
|
|
420
471
|
maxReplayRecords: Math.min(remainingRecords, Math.max(this.#mode === "history" ? 1 : 32, Math.floor(this.#initialReplayBytes / 1_024))),
|
|
421
472
|
historical: this.#mode === "history",
|
|
422
473
|
...(this.#mode === "live" ? { connections: this.#openCodeConnections } : { completedHistory: this.#completedHistoryDatabases }),
|
|
423
|
-
});
|
|
474
|
+
}));
|
|
475
|
+
if (sqlite === undefined)
|
|
476
|
+
continue;
|
|
424
477
|
records.push(...sqlite.records);
|
|
425
478
|
fileCount += sqlite.files;
|
|
426
479
|
malformed += sqlite.malformed;
|
|
@@ -437,7 +490,9 @@ export class JSONLSourceReader {
|
|
|
437
490
|
const checkpoint = Object.freeze({
|
|
438
491
|
generation: ++this.#generation,
|
|
439
492
|
cursors: new Map(this.#cursors),
|
|
493
|
+
scannedAt,
|
|
440
494
|
});
|
|
495
|
+
this.#scanStart = checkpoint.cursors;
|
|
441
496
|
const jsonlComplete = this.#mode !== "history" || selected.length === 0;
|
|
442
497
|
return {
|
|
443
498
|
records,
|
|
@@ -458,13 +513,113 @@ export class JSONLSourceReader {
|
|
|
458
513
|
if (checkpoint.generation > this.#generation) {
|
|
459
514
|
throw new Error("Activity source checkpoint is invalid.");
|
|
460
515
|
}
|
|
516
|
+
// Saved once more when the time is missing, so the next start knows
|
|
517
|
+
// when this one ran. Later idle scans keep the time of the last change:
|
|
518
|
+
// a file read since then would have changed the cursors.
|
|
461
519
|
if (this.#savedCursors === undefined ||
|
|
520
|
+
this.#savedScannedAt === undefined ||
|
|
462
521
|
!sameCursors(checkpoint.cursors, this.#savedCursors)) {
|
|
463
|
-
await this.#cursorStore.save(checkpoint.cursors);
|
|
522
|
+
await this.#cursorStore.save(checkpoint.cursors, checkpoint.scannedAt);
|
|
464
523
|
this.#savedCursors = checkpoint.cursors;
|
|
524
|
+
this.#savedScannedAt = checkpoint.scannedAt;
|
|
465
525
|
}
|
|
466
526
|
this.#committedGeneration = checkpoint.generation;
|
|
467
527
|
}
|
|
528
|
+
/**
|
|
529
|
+
* Where the next scan starts. A scan moves the cursors past every record
|
|
530
|
+
* it returns, before the caller delivers them; `rewind` puts the reader
|
|
531
|
+
* back here when they could not be delivered, so the next scan returns
|
|
532
|
+
* the same records instead of skipping them.
|
|
533
|
+
*/
|
|
534
|
+
position() {
|
|
535
|
+
return {
|
|
536
|
+
cursors: this.#scanStart,
|
|
537
|
+
oversizedContinuations: new Set(this.#oversizedContinuations),
|
|
538
|
+
completedHistoryFiles: new Map(this.#completedHistoryFiles),
|
|
539
|
+
completedHistoryDatabases: new Map(this.#completedHistoryDatabases),
|
|
540
|
+
initialScanComplete: this.#initialScanComplete,
|
|
541
|
+
unbaselinedSources: new Set(this.#unbaselinedSources),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
rewind(position) {
|
|
545
|
+
if (position.cursors === undefined) {
|
|
546
|
+
// Taken before the first scan: reload what is durable.
|
|
547
|
+
this.#cursors = undefined;
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
this.#cursors = new RecentCursors();
|
|
551
|
+
for (const [key, offset] of position.cursors) {
|
|
552
|
+
this.#cursors.set(key, offset);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
this.#scanStart = position.cursors;
|
|
556
|
+
this.#oversizedContinuations = new Set(position.oversizedContinuations);
|
|
557
|
+
this.#completedHistoryFiles = new Map(position.completedHistoryFiles);
|
|
558
|
+
this.#completedHistoryDatabases = new Map(position.completedHistoryDatabases);
|
|
559
|
+
this.#initialScanComplete = position.initialScanComplete;
|
|
560
|
+
this.#unbaselinedSources = new Set(position.unbaselinedSources);
|
|
561
|
+
}
|
|
562
|
+
/** Sources and files that failed and are skipped until `retryAt`. */
|
|
563
|
+
sourceFailures() {
|
|
564
|
+
return [...this.#failures.values()];
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Run one source's scan so that its failure cannot fail the others. A
|
|
568
|
+
* failed source is recorded, skipped until its retry time, and its cursor
|
|
569
|
+
* changes are undone so none of its records is skipped. Returns undefined
|
|
570
|
+
* when the source was skipped or failed.
|
|
571
|
+
*/
|
|
572
|
+
async #isolated(key, adapterId, baselines, scan) {
|
|
573
|
+
const remember = (missed) => {
|
|
574
|
+
if (this.#mode !== "live")
|
|
575
|
+
return;
|
|
576
|
+
if (missed && baselines)
|
|
577
|
+
this.#unbaselinedSources.add(key);
|
|
578
|
+
else if (!missed)
|
|
579
|
+
this.#unbaselinedSources.delete(key);
|
|
580
|
+
};
|
|
581
|
+
if (this.#skipping(key)) {
|
|
582
|
+
remember(true);
|
|
583
|
+
return undefined;
|
|
584
|
+
}
|
|
585
|
+
this.#cursors?.track();
|
|
586
|
+
try {
|
|
587
|
+
const result = await scan();
|
|
588
|
+
this.#cursors?.untrack(false);
|
|
589
|
+
this.#failures.delete(key);
|
|
590
|
+
remember(false);
|
|
591
|
+
return result;
|
|
592
|
+
}
|
|
593
|
+
catch (error) {
|
|
594
|
+
this.#cursors?.untrack(true);
|
|
595
|
+
this.#recordFailure(key, adapterId, error);
|
|
596
|
+
remember(true);
|
|
597
|
+
return undefined;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
#skipping(key) {
|
|
601
|
+
const failure = this.#failures.get(key);
|
|
602
|
+
return failure !== undefined && this.#clock() < failure.retryAt.getTime();
|
|
603
|
+
}
|
|
604
|
+
#recordFailure(key, adapterId, error) {
|
|
605
|
+
const failures = (this.#failures.get(key)?.failures ?? 0) + 1;
|
|
606
|
+
const now = this.#clock();
|
|
607
|
+
this.#failures.delete(key);
|
|
608
|
+
this.#failures.set(key, Object.freeze({
|
|
609
|
+
adapterId,
|
|
610
|
+
failures,
|
|
611
|
+
message: (error instanceof Error ? error.message : String(error))
|
|
612
|
+
.replace(/\s+/gu, " ")
|
|
613
|
+
.slice(0, 200),
|
|
614
|
+
failedAt: new Date(now),
|
|
615
|
+
retryAt: new Date(now + Math.min(failureRetryMaxMs, failureRetryBaseMs * 2 ** Math.min(failures - 1, 16))),
|
|
616
|
+
}));
|
|
617
|
+
for (const oldest of this.#failures.keys()) {
|
|
618
|
+
if (this.#failures.size <= maxTrackedFailures)
|
|
619
|
+
break;
|
|
620
|
+
this.#failures.delete(oldest);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
468
623
|
/**
|
|
469
624
|
* Close the database connections kept between scans. A later scan opens
|
|
470
625
|
* them again, so this only releases files, for example before shutdown.
|
|
@@ -606,11 +761,71 @@ export class JSONLSourceReader {
|
|
|
606
761
|
this.#setCursor(cursorId, legacyCursorId, offset + processedThrough);
|
|
607
762
|
return { records, malformed, oversized, processedRecords };
|
|
608
763
|
}
|
|
764
|
+
/** The files of one JSONL source, with their canonical paths. */
|
|
765
|
+
async #listJSONLSource(source, canonicalPaths) {
|
|
766
|
+
let files = [];
|
|
767
|
+
const pathPattern = source.pathPattern === undefined
|
|
768
|
+
? undefined
|
|
769
|
+
: new RegExp(source.pathPattern, "u");
|
|
770
|
+
for (const configuredRoot of source.roots) {
|
|
771
|
+
const root = expandedActivityRoot(configuredRoot);
|
|
772
|
+
const rootFiles = await listJSONLFiles(root);
|
|
773
|
+
let rootReal;
|
|
774
|
+
for (const file of rootFiles) {
|
|
775
|
+
if (pathPattern !== undefined &&
|
|
776
|
+
!pathPattern.test(relative(root, file.path).split(sep).join("/"))) {
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
let canonicalPath;
|
|
780
|
+
try {
|
|
781
|
+
// The listing never follows links, so a listed file keeps its
|
|
782
|
+
// canonical path while its root resolves to the same place and
|
|
783
|
+
// the file keeps its identity. Resolve each file only once.
|
|
784
|
+
rootReal ??= await realpath(root);
|
|
785
|
+
const key = `${rootReal}\u0000${file.path}`;
|
|
786
|
+
const identity = fileIdentityKey(file.identity);
|
|
787
|
+
const known = this.#canonicalPaths.get(key);
|
|
788
|
+
canonicalPath =
|
|
789
|
+
known !== undefined && identity !== undefined &&
|
|
790
|
+
known.identity === identity
|
|
791
|
+
? known.path
|
|
792
|
+
: await realpath(file.path);
|
|
793
|
+
if (identity !== undefined) {
|
|
794
|
+
canonicalPaths.set(key, { path: canonicalPath, identity });
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
catch (error) {
|
|
798
|
+
if (isTransientSourceError(error))
|
|
799
|
+
continue;
|
|
800
|
+
throw error;
|
|
801
|
+
}
|
|
802
|
+
files.push({
|
|
803
|
+
adapterId: source.adapterId,
|
|
804
|
+
...(source.sourceDisplayName === undefined
|
|
805
|
+
? {}
|
|
806
|
+
: { sourceDisplayName: source.sourceDisplayName }),
|
|
807
|
+
root,
|
|
808
|
+
path: canonicalPath,
|
|
809
|
+
modifiedMs: file.modifiedMs,
|
|
810
|
+
identity: file.identity,
|
|
811
|
+
});
|
|
812
|
+
if (files.length > maxCursorEntries * 2) {
|
|
813
|
+
files = reconciledSourceFiles(files);
|
|
814
|
+
files.sort(recentFileFirst);
|
|
815
|
+
files.length = maxCursorEntries;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return files;
|
|
820
|
+
}
|
|
609
821
|
async #pendingHistoryFiles(files) {
|
|
610
822
|
const pending = [];
|
|
611
823
|
for (const file of files) {
|
|
612
824
|
if (this.#completedHistoryFiles.get(file.path) === file.modifiedMs)
|
|
613
825
|
continue;
|
|
826
|
+
// A failing file is retried later without holding the import open.
|
|
827
|
+
if (this.#skipping(fileKey(file.path)))
|
|
828
|
+
continue;
|
|
614
829
|
try {
|
|
615
830
|
const metadata = await stat(file.path);
|
|
616
831
|
const offset = this.#cursors?.get(fileCursorId(file.path, metadata)) ??
|
|
@@ -625,8 +840,9 @@ export class JSONLSourceReader {
|
|
|
625
840
|
}
|
|
626
841
|
}
|
|
627
842
|
catch (error) {
|
|
628
|
-
if (!isTransientSourceError(error))
|
|
629
|
-
|
|
843
|
+
if (!isTransientSourceError(error)) {
|
|
844
|
+
this.#recordFailure(fileKey(file.path), file.adapterId, error);
|
|
845
|
+
}
|
|
630
846
|
}
|
|
631
847
|
}
|
|
632
848
|
return pending;
|
|
@@ -655,7 +871,11 @@ export class JSONLSourceReader {
|
|
|
655
871
|
return;
|
|
656
872
|
if (this.#cursors?.has(fileCursorId(fileReal, metadata)) || this.#cursors?.has(digest(fileReal)))
|
|
657
873
|
return;
|
|
658
|
-
|
|
874
|
+
// A session that began while the observer was stopped is read from its
|
|
875
|
+
// start; any other file without a cursor starts at its end, as before.
|
|
876
|
+
this.#setCursor(fileCursorId(fileReal, metadata), digest(fileReal), this.#lastRunAt !== undefined && startedAfter(metadata, this.#lastRunAt)
|
|
877
|
+
? 0
|
|
878
|
+
: metadata.size);
|
|
659
879
|
}
|
|
660
880
|
}
|
|
661
881
|
export function expandedActivityRoot(root) {
|
|
@@ -820,6 +1040,18 @@ function fileIdentityKey(identity) {
|
|
|
820
1040
|
}
|
|
821
1041
|
return `${identity.dev}:${identity.ino}:${identity.birthtimeMs}`;
|
|
822
1042
|
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Whether saved `cursors` are at the end of the file at `path`, so every
|
|
1045
|
+
* record in it was read. A file that grew or was replaced since does not
|
|
1046
|
+
* count.
|
|
1047
|
+
*/
|
|
1048
|
+
export async function fileReadThrough(path, cursors) {
|
|
1049
|
+
const fileReal = await realpath(path);
|
|
1050
|
+
const metadata = await stat(fileReal);
|
|
1051
|
+
const offset = cursors.get(fileCursorId(fileReal, metadata)) ??
|
|
1052
|
+
cursors.get(digest(fileReal));
|
|
1053
|
+
return offset === metadata.size;
|
|
1054
|
+
}
|
|
823
1055
|
function fileCursorId(path, metadata) {
|
|
824
1056
|
const identity = metadata.ino > 0 || metadata.dev > 0
|
|
825
1057
|
? `inode:${metadata.dev}:${metadata.ino}:${metadata.birthtimeMs}`
|
|
@@ -843,6 +1075,12 @@ function newestFilesPerAdapter(files, limit) {
|
|
|
843
1075
|
function configuredRoots(sources, format) {
|
|
844
1076
|
return new Set(sources.flatMap((source) => source.format === format ? source.roots.map(expandedActivityRoot) : []));
|
|
845
1077
|
}
|
|
1078
|
+
function sourceKey(source) {
|
|
1079
|
+
return JSON.stringify([source.format ?? "jsonl", source.adapterId, source.roots, source.pathPattern ?? ""]);
|
|
1080
|
+
}
|
|
1081
|
+
function fileKey(path) {
|
|
1082
|
+
return `file\u0000${path}`;
|
|
1083
|
+
}
|
|
846
1084
|
function sameCursors(left, right) {
|
|
847
1085
|
if (left.size !== right.size)
|
|
848
1086
|
return false;
|
|
@@ -868,6 +1106,7 @@ function isTransientSourceError(error) {
|
|
|
868
1106
|
error.code === "EACCES" ||
|
|
869
1107
|
error.code === "EPERM" ||
|
|
870
1108
|
error.code === "ENOTDIR" ||
|
|
1109
|
+
error.code === "EBUSY" ||
|
|
871
1110
|
error.code === "ESTALE"));
|
|
872
1111
|
}
|
|
873
1112
|
//# sourceMappingURL=source-reader.js.map
|