@phigon/observer 8.1.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 +244 -18
- 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/action-policy.d.ts.map +1 -1
- package/dist/src/action-policy.js +4 -2
- package/dist/src/action-policy.js.map +1 -1
- package/dist/src/activity/adapters/claude.d.ts.map +1 -1
- package/dist/src/activity/adapters/claude.js +28 -2
- package/dist/src/activity/adapters/claude.js.map +1 -1
- package/dist/src/activity/adapters/cline-task.d.ts +7 -0
- package/dist/src/activity/adapters/cline-task.d.ts.map +1 -0
- package/dist/src/activity/adapters/cline-task.js +197 -0
- package/dist/src/activity/adapters/cline-task.js.map +1 -0
- package/dist/src/activity/adapters/cline.d.ts +3 -0
- package/dist/src/activity/adapters/cline.d.ts.map +1 -0
- package/dist/src/activity/adapters/cline.js +102 -0
- package/dist/src/activity/adapters/cline.js.map +1 -0
- package/dist/src/activity/adapters/codex.d.ts.map +1 -1
- package/dist/src/activity/adapters/codex.js +76 -0
- package/dist/src/activity/adapters/codex.js.map +1 -1
- package/dist/src/activity/adapters/copilot-chat.d.ts +3 -0
- package/dist/src/activity/adapters/copilot-chat.d.ts.map +1 -0
- package/dist/src/activity/adapters/copilot-chat.js +176 -0
- package/dist/src/activity/adapters/copilot-chat.js.map +1 -0
- package/dist/src/activity/adapters/copilot.d.ts +2 -1
- package/dist/src/activity/adapters/copilot.d.ts.map +1 -1
- package/dist/src/activity/adapters/copilot.js +173 -2
- package/dist/src/activity/adapters/copilot.js.map +1 -1
- package/dist/src/activity/adapters/cursor.d.ts +3 -0
- package/dist/src/activity/adapters/cursor.d.ts.map +1 -0
- package/dist/src/activity/adapters/cursor.js +86 -0
- package/dist/src/activity/adapters/cursor.js.map +1 -0
- package/dist/src/activity/adapters/factory-droid.d.ts +3 -0
- package/dist/src/activity/adapters/factory-droid.d.ts.map +1 -0
- package/dist/src/activity/adapters/factory-droid.js +126 -0
- package/dist/src/activity/adapters/factory-droid.js.map +1 -0
- package/dist/src/activity/adapters/gemini.d.ts +3 -0
- package/dist/src/activity/adapters/gemini.d.ts.map +1 -0
- package/dist/src/activity/adapters/gemini.js +177 -0
- package/dist/src/activity/adapters/gemini.js.map +1 -0
- package/dist/src/activity/adapters/goose.d.ts +3 -0
- package/dist/src/activity/adapters/goose.d.ts.map +1 -0
- package/dist/src/activity/adapters/goose.js +116 -0
- package/dist/src/activity/adapters/goose.js.map +1 -0
- package/dist/src/activity/adapters/kiro.d.ts +3 -0
- package/dist/src/activity/adapters/kiro.d.ts.map +1 -0
- package/dist/src/activity/adapters/kiro.js +47 -0
- package/dist/src/activity/adapters/kiro.js.map +1 -0
- 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/adapters/opencode.d.ts.map +1 -1
- package/dist/src/activity/adapters/opencode.js +108 -1
- package/dist/src/activity/adapters/opencode.js.map +1 -1
- package/dist/src/activity/adapters/zed.d.ts +3 -0
- package/dist/src/activity/adapters/zed.d.ts.map +1 -0
- package/dist/src/activity/adapters/zed.js +134 -0
- package/dist/src/activity/adapters/zed.js.map +1 -0
- package/dist/src/activity/collector.d.ts +43 -1
- package/dist/src/activity/collector.d.ts.map +1 -1
- package/dist/src/activity/collector.js +490 -77
- package/dist/src/activity/collector.js.map +1 -1
- package/dist/src/activity/goose-sqlite-source.d.ts +33 -0
- package/dist/src/activity/goose-sqlite-source.d.ts.map +1 -0
- package/dist/src/activity/goose-sqlite-source.js +274 -0
- package/dist/src/activity/goose-sqlite-source.js.map +1 -0
- package/dist/src/activity/index.d.ts +3 -0
- package/dist/src/activity/index.d.ts.map +1 -1
- package/dist/src/activity/index.js +3 -0
- package/dist/src/activity/index.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 +94 -4
- package/dist/src/activity/ingest.js.map +1 -1
- package/dist/src/activity/json-document-source.d.ts +151 -0
- package/dist/src/activity/json-document-source.d.ts.map +1 -0
- package/dist/src/activity/json-document-source.js +586 -0
- package/dist/src/activity/json-document-source.js.map +1 -0
- 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/normalize.d.ts.map +1 -1
- package/dist/src/activity/normalize.js +64 -6
- package/dist/src/activity/normalize.js.map +1 -1
- package/dist/src/activity/opencode-message-tables.d.ts +87 -0
- package/dist/src/activity/opencode-message-tables.d.ts.map +1 -0
- package/dist/src/activity/opencode-message-tables.js +546 -0
- package/dist/src/activity/opencode-message-tables.js.map +1 -0
- package/dist/src/activity/opencode-sqlite-source.d.ts +7 -0
- package/dist/src/activity/opencode-sqlite-source.d.ts.map +1 -1
- package/dist/src/activity/opencode-sqlite-source.js +154 -63
- package/dist/src/activity/opencode-sqlite-source.js.map +1 -1
- package/dist/src/activity/source-reader.d.ts +60 -4
- package/dist/src/activity/source-reader.d.ts.map +1 -1
- package/dist/src/activity/source-reader.js +593 -75
- package/dist/src/activity/source-reader.js.map +1 -1
- package/dist/src/activity/sqlite-connections.d.ts +77 -0
- package/dist/src/activity/sqlite-connections.d.ts.map +1 -0
- package/dist/src/activity/sqlite-connections.js +220 -0
- package/dist/src/activity/sqlite-connections.js.map +1 -0
- package/dist/src/activity/zed-sqlite-source.d.ts +65 -0
- package/dist/src/activity/zed-sqlite-source.d.ts.map +1 -0
- package/dist/src/activity/zed-sqlite-source.js +547 -0
- package/dist/src/activity/zed-sqlite-source.js.map +1 -0
- package/dist/src/agent-action-executor.d.ts +18 -1
- package/dist/src/agent-action-executor.d.ts.map +1 -1
- package/dist/src/agent-action-executor.js +113 -58
- package/dist/src/agent-action-executor.js.map +1 -1
- package/dist/src/agentic-guardian.d.ts.map +1 -1
- package/dist/src/agentic-guardian.js +6 -5
- package/dist/src/agentic-guardian.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 +534 -50
- 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 +39 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +187 -36
- package/dist/src/config.js.map +1 -1
- package/dist/src/connection-config.d.ts.map +1 -1
- package/dist/src/connection-config.js +5 -0
- package/dist/src/connection-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 +51 -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 +29 -4
- package/dist/src/credential-store.js.map +1 -1
- package/dist/src/custom-agent-registry.d.ts.map +1 -1
- package/dist/src/custom-agent-registry.js +2 -1
- package/dist/src/custom-agent-registry.js.map +1 -1
- package/dist/src/discovery/adapters/generic.d.ts.map +1 -1
- package/dist/src/discovery/adapters/generic.js +160 -2
- package/dist/src/discovery/adapters/generic.js.map +1 -1
- package/dist/src/discovery/engine.d.ts +7 -1
- package/dist/src/discovery/engine.d.ts.map +1 -1
- package/dist/src/discovery/engine.js +21 -16
- package/dist/src/discovery/engine.js.map +1 -1
- package/dist/src/discovery/known-agent.d.ts +7 -0
- package/dist/src/discovery/known-agent.d.ts.map +1 -1
- package/dist/src/discovery/known-agent.js +9 -4
- package/dist/src/discovery/known-agent.js.map +1 -1
- package/dist/src/discovery/system-evidence.d.ts +40 -1
- package/dist/src/discovery/system-evidence.d.ts.map +1 -1
- package/dist/src/discovery/system-evidence.js +189 -16
- package/dist/src/discovery/system-evidence.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 +137 -42
- 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 +341 -44
- 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-containment.d.ts +2 -6
- package/dist/src/guardian-containment.d.ts.map +1 -1
- package/dist/src/guardian-containment.js +12 -63
- package/dist/src/guardian-containment.js.map +1 -1
- package/dist/src/guardian-decision-journal.d.ts.map +1 -1
- package/dist/src/guardian-decision-journal.js +4 -19
- package/dist/src/guardian-decision-journal.js.map +1 -1
- package/dist/src/guardian-managed-exec.d.ts.map +1 -1
- package/dist/src/guardian-managed-exec.js +6 -18
- package/dist/src/guardian-managed-exec.js.map +1 -1
- package/dist/src/guardian-model-bridge.d.ts.map +1 -1
- package/dist/src/guardian-model-bridge.js +2 -1
- package/dist/src/guardian-model-bridge.js.map +1 -1
- package/dist/src/guardian-native.d.ts +0 -1
- package/dist/src/guardian-native.d.ts.map +1 -1
- package/dist/src/guardian-native.js +0 -6
- package/dist/src/guardian-native.js.map +1 -1
- package/dist/src/guardian-process-monitor.d.ts +4 -0
- package/dist/src/guardian-process-monitor.d.ts.map +1 -1
- package/dist/src/guardian-process-monitor.js +82 -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 +28 -10
- 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 +6 -3
- 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 +22 -7
- package/dist/src/machine-command-executor.d.ts.map +1 -1
- package/dist/src/machine-command-executor.js +80 -19
- 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 -65
- package/dist/src/pair.js.map +1 -1
- package/dist/src/private-directory.d.ts +10 -0
- package/dist/src/private-directory.d.ts.map +1 -0
- package/dist/src/private-directory.js +66 -0
- package/dist/src/private-directory.js.map +1 -0
- package/dist/src/process-tree.d.ts +73 -0
- package/dist/src/process-tree.d.ts.map +1 -0
- package/dist/src/process-tree.js +256 -0
- package/dist/src/process-tree.js.map +1 -0
- 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 +29 -0
- package/dist/src/server-authority.d.ts.map +1 -0
- package/dist/src/server-authority.js +106 -0
- package/dist/src/server-authority.js.map +1 -0
- 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 +21 -1
- package/dist/src/stream-client.d.ts.map +1 -1
- package/dist/src/stream-client.js +137 -24
- 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 +5 -2
- package/dist/src/stream-state.js.map +1 -1
- package/dist/src/system-inspection.d.ts +78 -0
- package/dist/src/system-inspection.d.ts.map +1 -0
- package/dist/src/system-inspection.js +281 -0
- package/dist/src/system-inspection.js.map +1 -0
- package/dist/src/update.d.ts.map +1 -1
- package/dist/src/update.js +9 -3
- package/dist/src/update.js.map +1 -1
- package/dist/src/user-notices.d.ts +64 -0
- package/dist/src/user-notices.d.ts.map +1 -0
- package/dist/src/user-notices.js +141 -0
- package/dist/src/user-notices.js.map +1 -0
- package/package.json +7 -7
- package/dist/src/windows-process.d.ts +0 -7
- package/dist/src/windows-process.d.ts.map +0 -1
- package/dist/src/windows-process.js +0 -18
- package/dist/src/windows-process.js.map +0 -1
|
@@ -1,16 +1,93 @@
|
|
|
1
|
+
import { makeDirectoryPrivate } from "../private-directory.js";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import { chmod, mkdir, open, readFile, readdir, realpath, stat, unlink, writeFile, } from "node:fs/promises";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import { basename, join, relative, resolve, sep } from "node:path";
|
|
5
6
|
import { atomicReplace } from "../atomic-replace.js";
|
|
7
|
+
import { DirectoryListingCache, scanJSONDocumentSource, startedAfter, } from "./json-document-source.js";
|
|
8
|
+
import { SQLiteConnections } from "./sqlite-connections.js";
|
|
6
9
|
const maxSourceFiles = 256;
|
|
7
10
|
const historicalFilesPerScan = 16;
|
|
8
11
|
const historicalRecordsPerScan = 4;
|
|
9
12
|
const maxDirectoryDepth = 8;
|
|
10
13
|
const maxReadBytesPerFile = 8 << 20;
|
|
11
14
|
const defaultInitialReplayBytes = 256 << 10;
|
|
12
|
-
|
|
15
|
+
// Normal read window. A longer complete record widens the read up to the line
|
|
16
|
+
// limit; the line limit stays below the per-file read limit.
|
|
17
|
+
const readWindowBytes = 1 << 20;
|
|
18
|
+
const maxLineBytes = 6 << 20;
|
|
13
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;
|
|
26
|
+
/**
|
|
27
|
+
* Source cursors in least-recently-used order, so a full store evicts the
|
|
28
|
+
* cursors of sessions nobody reads any more instead of live ones (an
|
|
29
|
+
* evicted live cursor is re-baselined after a restart, which skips what
|
|
30
|
+
* was written while the observer was stopped). Reads are only recorded and
|
|
31
|
+
* applied by `settle`, so iterating the map never observes reordering.
|
|
32
|
+
*/
|
|
33
|
+
class RecentCursors extends Map {
|
|
34
|
+
#used = new Set();
|
|
35
|
+
// Values from before `track`, for each cursor changed since.
|
|
36
|
+
#journal;
|
|
37
|
+
get(key) {
|
|
38
|
+
const value = super.get(key);
|
|
39
|
+
if (value !== undefined)
|
|
40
|
+
this.#used.add(key);
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
set(key, value) {
|
|
44
|
+
this.#remember(key);
|
|
45
|
+
super.delete(key);
|
|
46
|
+
return super.set(key, value);
|
|
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
|
+
}
|
|
74
|
+
/** Move cursors read since the last call to the recent end, then bound. */
|
|
75
|
+
settle(limit) {
|
|
76
|
+
for (const key of this.#used) {
|
|
77
|
+
const value = super.get(key);
|
|
78
|
+
if (value === undefined)
|
|
79
|
+
continue;
|
|
80
|
+
super.delete(key);
|
|
81
|
+
super.set(key, value);
|
|
82
|
+
}
|
|
83
|
+
this.#used.clear();
|
|
84
|
+
for (const key of super.keys()) {
|
|
85
|
+
if (this.size <= limit)
|
|
86
|
+
break;
|
|
87
|
+
super.delete(key);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
14
91
|
export class ActivityCursorStore {
|
|
15
92
|
#dataDirectory;
|
|
16
93
|
#path;
|
|
@@ -25,7 +102,7 @@ export class ActivityCursorStore {
|
|
|
25
102
|
}
|
|
26
103
|
catch (error) {
|
|
27
104
|
if (isNodeError(error) && error.code === "ENOENT")
|
|
28
|
-
return new Map();
|
|
105
|
+
return { cursors: new Map() };
|
|
29
106
|
throw error;
|
|
30
107
|
}
|
|
31
108
|
let value;
|
|
@@ -42,7 +119,7 @@ export class ActivityCursorStore {
|
|
|
42
119
|
const recovered = new Map();
|
|
43
120
|
await this.save(recovered);
|
|
44
121
|
process.emitWarning("Saved activity checkpoints were unreadable. Available source history will be rescanned using stable record IDs.", { code: "PHIGON_CURSOR_RECOVERY" });
|
|
45
|
-
return recovered;
|
|
122
|
+
return { cursors: recovered };
|
|
46
123
|
}
|
|
47
124
|
if (typeof value !== "object" ||
|
|
48
125
|
value === null ||
|
|
@@ -53,7 +130,8 @@ export class ActivityCursorStore {
|
|
|
53
130
|
value.files === null) {
|
|
54
131
|
throw new Error("Activity cursor state is invalid.");
|
|
55
132
|
}
|
|
56
|
-
|
|
133
|
+
// Saved oldest-first (see save); keep the most recently used cursors.
|
|
134
|
+
const entries = Object.entries(value.files).slice(-maxCursorEntries);
|
|
57
135
|
const result = new Map();
|
|
58
136
|
for (const [key, offset] of entries) {
|
|
59
137
|
if (/^[a-f0-9]{64}$/u.test(key) &&
|
|
@@ -63,15 +141,24 @@ export class ActivityCursorStore {
|
|
|
63
141
|
result.set(key, offset);
|
|
64
142
|
}
|
|
65
143
|
}
|
|
66
|
-
|
|
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 };
|
|
67
150
|
}
|
|
68
|
-
async save(cursors) {
|
|
151
|
+
async save(cursors, scannedAt) {
|
|
69
152
|
await mkdir(this.#dataDirectory, { recursive: true, mode: 0o700 });
|
|
70
|
-
await
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const state = {
|
|
153
|
+
await makeDirectoryPrivate(this.#dataDirectory);
|
|
154
|
+
// Keep the map's order (least recently used first) so a restarted
|
|
155
|
+
// reader still knows which cursors are stale.
|
|
156
|
+
const files = Object.fromEntries([...cursors.entries()].slice(-maxCursorEntries));
|
|
157
|
+
const state = {
|
|
158
|
+
version: 1,
|
|
159
|
+
files,
|
|
160
|
+
...(scannedAt === undefined ? {} : { scanned_at: new Date(scannedAt).toISOString() }),
|
|
161
|
+
};
|
|
75
162
|
const temporary = `${this.#path}.${process.pid}.${randomUUID()}.tmp`;
|
|
76
163
|
try {
|
|
77
164
|
await writeFile(temporary, `${JSON.stringify(state)}\n`, {
|
|
@@ -101,84 +188,120 @@ export class JSONLSourceReader {
|
|
|
101
188
|
#initialReplaySessions;
|
|
102
189
|
#mode;
|
|
103
190
|
#cursors;
|
|
191
|
+
// The cursor state that is already durable. An idle scan produces an
|
|
192
|
+
// identical checkpoint every tick; rewriting and flushing it would cost a
|
|
193
|
+
// synchronous disk write per scan interval without changing anything.
|
|
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;
|
|
104
203
|
#initialScanComplete = false;
|
|
105
204
|
#generation = 0;
|
|
106
205
|
#committedGeneration = 0;
|
|
107
206
|
#sourceCache;
|
|
207
|
+
// Canonical paths from the previous listing, keyed by resolved root and
|
|
208
|
+
// listed path. Holds only files seen in the latest listing.
|
|
209
|
+
#canonicalPaths = new Map();
|
|
108
210
|
#completedHistoryFiles = new Map();
|
|
109
|
-
|
|
211
|
+
#completedHistoryDatabases = new Map();
|
|
212
|
+
// Cursors that stopped inside a record above the line limit. The next read
|
|
213
|
+
// skips to that record's end instead of parsing its tail as a new record.
|
|
214
|
+
#oversizedContinuations = new Set();
|
|
215
|
+
// Live database connections kept between scans, so an unchanged database
|
|
216
|
+
// is recognized without reopening it and rerunning every session query.
|
|
217
|
+
#openCodeConnections = new SQLiteConnections();
|
|
218
|
+
// Folder listings for JSON-document agents, reused while folders are unchanged.
|
|
219
|
+
#documentListings = new DirectoryListingCache();
|
|
220
|
+
#gooseConnections = new SQLiteConnections();
|
|
221
|
+
#zedConnections = new SQLiteConnections();
|
|
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) {
|
|
110
229
|
this.#cursorStore = new ActivityCursorStore(dataDirectory);
|
|
111
230
|
this.#replayOnFirstStart = replayOnFirstStart;
|
|
112
231
|
this.#initialReplayBytes = Math.min(maxReadBytesPerFile, Math.max(1, initialReplayBytes));
|
|
113
232
|
this.#initialReplaySessions = Math.min(maxSourceFiles, Math.max(1, initialReplaySessions));
|
|
114
233
|
this.#mode = mode;
|
|
234
|
+
this.#clock = clock;
|
|
115
235
|
}
|
|
116
236
|
async scan(sources, maxRecords) {
|
|
117
|
-
this.#cursors
|
|
237
|
+
if (this.#cursors === undefined) {
|
|
238
|
+
const loaded = await this.#cursorStore.load();
|
|
239
|
+
this.#cursors = new RecentCursors();
|
|
240
|
+
for (const [key, offset] of loaded.cursors)
|
|
241
|
+
this.#cursors.set(key, offset);
|
|
242
|
+
this.#savedCursors = loaded.cursors;
|
|
243
|
+
this.#savedScannedAt = loaded.scannedAt;
|
|
244
|
+
this.#lastRunAt = loaded.scannedAt;
|
|
245
|
+
this.#scanStart = loaded.cursors;
|
|
246
|
+
}
|
|
247
|
+
const scannedAt = this.#clock();
|
|
118
248
|
const records = [];
|
|
119
249
|
let fileCount = 0;
|
|
120
250
|
let malformed = 0;
|
|
121
251
|
let oversized = 0;
|
|
122
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();
|
|
123
256
|
const cacheKey = JSON.stringify(sources);
|
|
124
257
|
if (this.#mode === "history" && this.#sourceCache?.key === cacheKey && Date.now() - this.#sourceCache.at < 5_000) {
|
|
125
258
|
candidates = this.#sourceCache.files;
|
|
126
259
|
}
|
|
127
260
|
else {
|
|
261
|
+
const canonicalPaths = new Map();
|
|
128
262
|
for (const source of sources) {
|
|
129
|
-
if (source.format
|
|
263
|
+
if (source.format !== undefined && source.format !== "jsonl")
|
|
130
264
|
continue;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
candidates.push({
|
|
145
|
-
adapterId: source.adapterId,
|
|
146
|
-
...(source.sourceDisplayName === undefined
|
|
147
|
-
? {}
|
|
148
|
-
: { sourceDisplayName: source.sourceDisplayName }),
|
|
149
|
-
root,
|
|
150
|
-
path: canonicalPath,
|
|
151
|
-
modifiedMs: file.modifiedMs,
|
|
152
|
-
});
|
|
153
|
-
if (candidates.length > maxCursorEntries * 2) {
|
|
154
|
-
candidates = reconciledSourceFiles(candidates);
|
|
155
|
-
candidates.sort(recentFileFirst);
|
|
156
|
-
candidates.length = maxCursorEntries;
|
|
157
|
-
}
|
|
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;
|
|
158
278
|
}
|
|
159
279
|
}
|
|
160
280
|
}
|
|
161
281
|
this.#sourceCache = { key: cacheKey, at: Date.now(), files: candidates };
|
|
282
|
+
this.#canonicalPaths = canonicalPaths;
|
|
162
283
|
}
|
|
163
284
|
const recentCandidates = reconciledSourceFiles(candidates)
|
|
164
285
|
.sort(recentFileFirst)
|
|
165
286
|
.slice(0, maxCursorEntries);
|
|
166
|
-
const initialScan = !this.#initialScanComplete;
|
|
167
287
|
const selected = this.#mode === "history"
|
|
168
288
|
? await this.#pendingHistoryFiles(recentCandidates)
|
|
169
289
|
: recentCandidates.slice(0, maxSourceFiles);
|
|
170
290
|
const replayFiles = initialScan && this.#replayOnFirstStart
|
|
171
291
|
? newestFilesPerAdapter(selected, this.#initialReplaySessions)
|
|
172
292
|
: new Set();
|
|
173
|
-
if (
|
|
293
|
+
if (this.#mode === "live" && (initialScan || baselinePaths.size > 0)) {
|
|
174
294
|
for (const file of [...recentCandidates].reverse()) {
|
|
295
|
+
if (!initialScan && !baselinePaths.has(file.path))
|
|
296
|
+
continue;
|
|
175
297
|
if (!replayFiles.has(file.path)) {
|
|
176
298
|
try {
|
|
177
299
|
await this.#baselineFile(file.root, file.path);
|
|
178
300
|
}
|
|
179
301
|
catch (error) {
|
|
180
|
-
if (!isTransientSourceError(error))
|
|
181
|
-
|
|
302
|
+
if (!isTransientSourceError(error)) {
|
|
303
|
+
this.#recordFailure(fileKey(file.path), file.adapterId, error);
|
|
304
|
+
}
|
|
182
305
|
}
|
|
183
306
|
}
|
|
184
307
|
}
|
|
@@ -190,18 +313,30 @@ export class JSONLSourceReader {
|
|
|
190
313
|
const reservedDatabaseRecords = sources.some((source) => source.format === "opencode-sqlite")
|
|
191
314
|
? Math.floor(remainingRecords / 2)
|
|
192
315
|
: 0;
|
|
316
|
+
const rootRealPaths = new Map();
|
|
193
317
|
for (const file of selected) {
|
|
194
318
|
if (remainingRecords <= reservedDatabaseRecords)
|
|
195
319
|
break;
|
|
320
|
+
if (this.#unchangedLiveFile(file)) {
|
|
321
|
+
fileCount++;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (this.#skipping(fileKey(file.path)))
|
|
325
|
+
continue;
|
|
196
326
|
let result;
|
|
197
327
|
try {
|
|
198
|
-
|
|
328
|
+
const rootReal = rootRealPaths.get(file.root) ??
|
|
329
|
+
await realpath(file.root);
|
|
330
|
+
rootRealPaths.set(file.root, rootReal);
|
|
331
|
+
result = await this.#readFile(file.adapterId, rootReal, file.path, replayFiles.has(file.path), file.sourceDisplayName, remainingRecords - reservedDatabaseRecords);
|
|
199
332
|
}
|
|
200
333
|
catch (error) {
|
|
201
|
-
if (isTransientSourceError(error))
|
|
202
|
-
|
|
203
|
-
|
|
334
|
+
if (!isTransientSourceError(error)) {
|
|
335
|
+
this.#recordFailure(fileKey(file.path), file.adapterId, error);
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
204
338
|
}
|
|
339
|
+
this.#failures.delete(fileKey(file.path));
|
|
205
340
|
fileCount++;
|
|
206
341
|
records.push(...result.records);
|
|
207
342
|
malformed += result.malformed;
|
|
@@ -213,6 +348,105 @@ export class JSONLSourceReader {
|
|
|
213
348
|
this.#completedHistoryFiles.set(file.path, file.modifiedMs);
|
|
214
349
|
}
|
|
215
350
|
}
|
|
351
|
+
let documentsComplete = true;
|
|
352
|
+
let documentFiles = 0;
|
|
353
|
+
const cursors = this.#cursors;
|
|
354
|
+
for (const source of sources) {
|
|
355
|
+
if (source.format !== "json-messages")
|
|
356
|
+
continue;
|
|
357
|
+
const key = sourceKey(source);
|
|
358
|
+
const sourceInitial = initialScan || this.#unbaselinedSources.has(key);
|
|
359
|
+
const documents = await this.#isolated(key, source.adapterId, sourceInitial, () => scanJSONDocumentSource({
|
|
360
|
+
adapterId: source.adapterId,
|
|
361
|
+
roots: source.roots.map(expandedActivityRoot),
|
|
362
|
+
pathPattern: source.pathPattern ?? "\\.json$",
|
|
363
|
+
messagesField: source.messagesField ?? "messages",
|
|
364
|
+
...source.document,
|
|
365
|
+
}, {
|
|
366
|
+
cursors,
|
|
367
|
+
listings: this.#documentListings,
|
|
368
|
+
initialScan: sourceInitial,
|
|
369
|
+
replayOnFirstStart: this.#replayOnFirstStart,
|
|
370
|
+
replaySessions: this.#initialReplaySessions,
|
|
371
|
+
maxReplayRecords: Math.max(32, Math.floor(this.#initialReplayBytes / 1_024)),
|
|
372
|
+
maxRecords: Math.max(0, remainingRecords - reservedDatabaseRecords),
|
|
373
|
+
historical: this.#mode === "history",
|
|
374
|
+
...(this.#lastRunAt === undefined ? {} : { newSince: this.#lastRunAt }),
|
|
375
|
+
}));
|
|
376
|
+
if (documents === undefined)
|
|
377
|
+
continue;
|
|
378
|
+
records.push(...documents.records);
|
|
379
|
+
fileCount += documents.files;
|
|
380
|
+
malformed += documents.malformed;
|
|
381
|
+
oversized += documents.oversized;
|
|
382
|
+
documentFiles += documents.files;
|
|
383
|
+
remainingRecords -= documents.records.length;
|
|
384
|
+
documentsComplete = documentsComplete && documents.complete;
|
|
385
|
+
}
|
|
386
|
+
for (const source of sources) {
|
|
387
|
+
if (source.format !== "goose-sqlite")
|
|
388
|
+
continue;
|
|
389
|
+
if (remainingRecords <= reservedDatabaseRecords) {
|
|
390
|
+
documentsComplete = false;
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
// Loaded on demand for the same reason as the OpenCode source below.
|
|
394
|
+
const { scanGooseSQLiteSources } = await import("./goose-sqlite-source.js");
|
|
395
|
+
const key = sourceKey(source);
|
|
396
|
+
const sourceInitial = initialScan || this.#unbaselinedSources.has(key);
|
|
397
|
+
const goose = await this.#isolated(key, source.adapterId, sourceInitial, () => scanGooseSQLiteSources({
|
|
398
|
+
adapterId: source.adapterId,
|
|
399
|
+
roots: source.roots.map(expandedActivityRoot),
|
|
400
|
+
cursors,
|
|
401
|
+
initialScan: sourceInitial,
|
|
402
|
+
replayOnFirstStart: this.#replayOnFirstStart,
|
|
403
|
+
maxReplayRecords: Math.max(32, Math.floor(this.#initialReplayBytes / 1_024)),
|
|
404
|
+
maxRecords: remainingRecords - reservedDatabaseRecords,
|
|
405
|
+
historical: this.#mode === "history",
|
|
406
|
+
...(this.#mode === "live" ? { connections: this.#gooseConnections } : { completedHistory: this.#completedHistoryDatabases }),
|
|
407
|
+
}));
|
|
408
|
+
if (goose === undefined)
|
|
409
|
+
continue;
|
|
410
|
+
records.push(...goose.records);
|
|
411
|
+
fileCount += goose.files;
|
|
412
|
+
malformed += goose.malformed;
|
|
413
|
+
oversized += goose.oversized;
|
|
414
|
+
documentFiles += goose.files;
|
|
415
|
+
remainingRecords -= goose.records.length;
|
|
416
|
+
documentsComplete = documentsComplete && goose.complete;
|
|
417
|
+
}
|
|
418
|
+
for (const source of sources) {
|
|
419
|
+
if (source.format !== "zed-sqlite")
|
|
420
|
+
continue;
|
|
421
|
+
if (remainingRecords <= reservedDatabaseRecords) {
|
|
422
|
+
documentsComplete = false;
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
const { scanZedSQLiteSources } = await import("./zed-sqlite-source.js");
|
|
426
|
+
const key = sourceKey(source);
|
|
427
|
+
const sourceInitial = initialScan || this.#unbaselinedSources.has(key);
|
|
428
|
+
const zed = await this.#isolated(key, source.adapterId, sourceInitial, () => scanZedSQLiteSources({
|
|
429
|
+
adapterId: source.adapterId,
|
|
430
|
+
roots: source.roots.map(expandedActivityRoot),
|
|
431
|
+
cursors,
|
|
432
|
+
initialScan: sourceInitial,
|
|
433
|
+
replayOnFirstStart: this.#replayOnFirstStart,
|
|
434
|
+
replaySessions: this.#initialReplaySessions,
|
|
435
|
+
maxReplayRecords: Math.max(32, Math.floor(this.#initialReplayBytes / 1_024)),
|
|
436
|
+
maxRecords: remainingRecords - reservedDatabaseRecords,
|
|
437
|
+
historical: this.#mode === "history",
|
|
438
|
+
...(this.#mode === "live" ? { connections: this.#zedConnections } : { completedHistory: this.#completedHistoryDatabases }),
|
|
439
|
+
}));
|
|
440
|
+
if (zed === undefined)
|
|
441
|
+
continue;
|
|
442
|
+
records.push(...zed.records);
|
|
443
|
+
fileCount += zed.files;
|
|
444
|
+
malformed += zed.malformed;
|
|
445
|
+
oversized += zed.oversized;
|
|
446
|
+
documentFiles += zed.files;
|
|
447
|
+
remainingRecords -= zed.records.length;
|
|
448
|
+
documentsComplete = documentsComplete && zed.complete;
|
|
449
|
+
}
|
|
216
450
|
let sqliteComplete = true;
|
|
217
451
|
let sqliteFiles = 0;
|
|
218
452
|
for (const source of sources) {
|
|
@@ -226,15 +460,20 @@ export class JSONLSourceReader {
|
|
|
226
460
|
// when an OpenCode data source is actually scanned so commands such as
|
|
227
461
|
// pair, doctor, and help stay warning-free.
|
|
228
462
|
const { scanOpenCodeSQLiteSources } = await import("./opencode-sqlite-source.js");
|
|
229
|
-
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({
|
|
230
466
|
adapterId: source.adapterId,
|
|
231
467
|
roots: source.roots.map(expandedActivityRoot),
|
|
232
|
-
cursors
|
|
233
|
-
initialScan,
|
|
468
|
+
cursors,
|
|
469
|
+
initialScan: sourceInitial,
|
|
234
470
|
replayOnFirstStart: this.#replayOnFirstStart,
|
|
235
471
|
maxReplayRecords: Math.min(remainingRecords, Math.max(this.#mode === "history" ? 1 : 32, Math.floor(this.#initialReplayBytes / 1_024))),
|
|
236
472
|
historical: this.#mode === "history",
|
|
237
|
-
|
|
473
|
+
...(this.#mode === "live" ? { connections: this.#openCodeConnections } : { completedHistory: this.#completedHistoryDatabases }),
|
|
474
|
+
}));
|
|
475
|
+
if (sqlite === undefined)
|
|
476
|
+
continue;
|
|
238
477
|
records.push(...sqlite.records);
|
|
239
478
|
fileCount += sqlite.files;
|
|
240
479
|
malformed += sqlite.malformed;
|
|
@@ -243,23 +482,29 @@ export class JSONLSourceReader {
|
|
|
243
482
|
remainingRecords -= sqlite.records.length;
|
|
244
483
|
sqliteComplete = sqliteComplete && sqlite.complete;
|
|
245
484
|
}
|
|
485
|
+
this.#openCodeConnections.retain(configuredRoots(sources, "opencode-sqlite"));
|
|
486
|
+
this.#gooseConnections.retain(configuredRoots(sources, "goose-sqlite"));
|
|
487
|
+
this.#zedConnections.retain(configuredRoots(sources, "zed-sqlite"));
|
|
246
488
|
this.#initialScanComplete = true;
|
|
489
|
+
this.#cursors.settle(maxCursorEntries);
|
|
247
490
|
const checkpoint = Object.freeze({
|
|
248
491
|
generation: ++this.#generation,
|
|
249
492
|
cursors: new Map(this.#cursors),
|
|
493
|
+
scannedAt,
|
|
250
494
|
});
|
|
495
|
+
this.#scanStart = checkpoint.cursors;
|
|
251
496
|
const jsonlComplete = this.#mode !== "history" || selected.length === 0;
|
|
252
497
|
return {
|
|
253
498
|
records,
|
|
254
499
|
metrics: {
|
|
255
500
|
files: fileCount,
|
|
256
|
-
discoveredFiles: recentCandidates.length + sqliteFiles,
|
|
501
|
+
discoveredFiles: recentCandidates.length + sqliteFiles + documentFiles,
|
|
257
502
|
records: records.length,
|
|
258
503
|
malformed,
|
|
259
504
|
oversized,
|
|
260
505
|
},
|
|
261
506
|
checkpoint,
|
|
262
|
-
complete: jsonlComplete && sqliteComplete,
|
|
507
|
+
complete: jsonlComplete && sqliteComplete && documentsComplete,
|
|
263
508
|
};
|
|
264
509
|
}
|
|
265
510
|
async commit(checkpoint) {
|
|
@@ -268,11 +513,135 @@ export class JSONLSourceReader {
|
|
|
268
513
|
if (checkpoint.generation > this.#generation) {
|
|
269
514
|
throw new Error("Activity source checkpoint is invalid.");
|
|
270
515
|
}
|
|
271
|
-
|
|
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.
|
|
519
|
+
if (this.#savedCursors === undefined ||
|
|
520
|
+
this.#savedScannedAt === undefined ||
|
|
521
|
+
!sameCursors(checkpoint.cursors, this.#savedCursors)) {
|
|
522
|
+
await this.#cursorStore.save(checkpoint.cursors, checkpoint.scannedAt);
|
|
523
|
+
this.#savedCursors = checkpoint.cursors;
|
|
524
|
+
this.#savedScannedAt = checkpoint.scannedAt;
|
|
525
|
+
}
|
|
272
526
|
this.#committedGeneration = checkpoint.generation;
|
|
273
527
|
}
|
|
274
|
-
|
|
275
|
-
|
|
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
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Close the database connections kept between scans. A later scan opens
|
|
625
|
+
* them again, so this only releases files, for example before shutdown.
|
|
626
|
+
*/
|
|
627
|
+
close() {
|
|
628
|
+
this.#openCodeConnections.closeAll();
|
|
629
|
+
this.#gooseConnections.closeAll();
|
|
630
|
+
this.#zedConnections.closeAll();
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* A live file whose listed size equals its cursor has nothing to read:
|
|
634
|
+
* reading it would resolve, stat and find zero bytes. Its identity comes
|
|
635
|
+
* from the listing done in the same scan, so growth is never missed.
|
|
636
|
+
*/
|
|
637
|
+
#unchangedLiveFile(file) {
|
|
638
|
+
if (this.#mode !== "live" || file.identity === undefined)
|
|
639
|
+
return false;
|
|
640
|
+
const cursorId = fileCursorId(file.path, file.identity);
|
|
641
|
+
return (this.#cursors?.get(cursorId) === file.identity.size &&
|
|
642
|
+
!this.#oversizedContinuations.has(cursorId));
|
|
643
|
+
}
|
|
644
|
+
async #readFile(adapterId, rootReal, file, initialReplay, sourceDisplayName, maxRecords = Number.MAX_SAFE_INTEGER) {
|
|
276
645
|
const fileReal = await realpath(file);
|
|
277
646
|
if (!isWithin(rootReal, fileReal)) {
|
|
278
647
|
return { records: [], malformed: 0, oversized: 0, processedRecords: 0 };
|
|
@@ -293,34 +662,50 @@ export class JSONLSourceReader {
|
|
|
293
662
|
(initialReplay ? this.#initialReplayBytes : maxReadBytesPerFile)));
|
|
294
663
|
if (metadata.size < offset)
|
|
295
664
|
offset = 0;
|
|
296
|
-
|
|
665
|
+
let length = Math.min(metadata.size - offset, maxReadBytesPerFile, Math.max(readWindowBytes + 1, Math.min(1_024, maxRecords) * 16_384));
|
|
297
666
|
if (length <= 0) {
|
|
298
667
|
this.#setCursor(cursorId, legacyCursorId, metadata.size);
|
|
668
|
+
this.#oversizedContinuations.delete(cursorId);
|
|
299
669
|
return { records: [], malformed: 0, oversized: 0, processedRecords: 0 };
|
|
300
670
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
await handle.close();
|
|
671
|
+
// Starting mid-file, read one byte early: if it is a newline the first
|
|
672
|
+
// record starts exactly at the offset and must not be skipped as partial.
|
|
673
|
+
// A cursor left inside an oversized record also resumes after its end.
|
|
674
|
+
const continuingOversized = this.#oversizedContinuations.has(cursorId);
|
|
675
|
+
const resync = (knownOffset === undefined && offset > 0) || continuingOversized;
|
|
676
|
+
if (resync) {
|
|
677
|
+
offset -= 1;
|
|
678
|
+
length += 1;
|
|
310
679
|
}
|
|
311
|
-
|
|
680
|
+
let content = await readRange(fileReal, offset, length);
|
|
681
|
+
if (resync) {
|
|
312
682
|
const firstNewline = content.indexOf(0x0a);
|
|
313
683
|
if (firstNewline < 0) {
|
|
314
684
|
this.#setCursor(cursorId, legacyCursorId, offset + content.length);
|
|
315
|
-
|
|
685
|
+
this.#rememberOversizedContinuation(cursorId);
|
|
686
|
+
return continuingOversized
|
|
687
|
+
? { records: [], malformed: 0, oversized: 0, processedRecords: 1 }
|
|
688
|
+
: { records: [], malformed: 0, oversized: 1, processedRecords: 1 };
|
|
316
689
|
}
|
|
690
|
+
this.#oversizedContinuations.delete(cursorId);
|
|
317
691
|
offset += firstNewline + 1;
|
|
318
692
|
content = content.subarray(firstNewline + 1);
|
|
319
693
|
}
|
|
320
|
-
|
|
694
|
+
let lastNewline = content.lastIndexOf(0x0a);
|
|
695
|
+
if (lastNewline < 0) {
|
|
696
|
+
// One complete record can be larger than the normal read window, for
|
|
697
|
+
// example a tool result or a pasted screenshot. Widen once, bounded by
|
|
698
|
+
// the record limit, instead of discarding it as oversized.
|
|
699
|
+
const widened = Math.min(metadata.size - offset, maxLineBytes + 1);
|
|
700
|
+
if (widened > content.length) {
|
|
701
|
+
content = await readRange(fileReal, offset, widened);
|
|
702
|
+
lastNewline = content.lastIndexOf(0x0a);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
321
705
|
if (lastNewline < 0) {
|
|
322
706
|
if (content.length > maxLineBytes) {
|
|
323
707
|
this.#setCursor(cursorId, legacyCursorId, offset + content.length);
|
|
708
|
+
this.#rememberOversizedContinuation(cursorId);
|
|
324
709
|
return { records: [], malformed: 0, oversized: 1, processedRecords: 1 };
|
|
325
710
|
}
|
|
326
711
|
return { records: [], malformed: 0, oversized: 0, processedRecords: 0 };
|
|
@@ -376,11 +761,71 @@ export class JSONLSourceReader {
|
|
|
376
761
|
this.#setCursor(cursorId, legacyCursorId, offset + processedThrough);
|
|
377
762
|
return { records, malformed, oversized, processedRecords };
|
|
378
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
|
+
}
|
|
379
821
|
async #pendingHistoryFiles(files) {
|
|
380
822
|
const pending = [];
|
|
381
823
|
for (const file of files) {
|
|
382
824
|
if (this.#completedHistoryFiles.get(file.path) === file.modifiedMs)
|
|
383
825
|
continue;
|
|
826
|
+
// A failing file is retried later without holding the import open.
|
|
827
|
+
if (this.#skipping(fileKey(file.path)))
|
|
828
|
+
continue;
|
|
384
829
|
try {
|
|
385
830
|
const metadata = await stat(file.path);
|
|
386
831
|
const offset = this.#cursors?.get(fileCursorId(file.path, metadata)) ??
|
|
@@ -395,12 +840,21 @@ export class JSONLSourceReader {
|
|
|
395
840
|
}
|
|
396
841
|
}
|
|
397
842
|
catch (error) {
|
|
398
|
-
if (!isTransientSourceError(error))
|
|
399
|
-
|
|
843
|
+
if (!isTransientSourceError(error)) {
|
|
844
|
+
this.#recordFailure(fileKey(file.path), file.adapterId, error);
|
|
845
|
+
}
|
|
400
846
|
}
|
|
401
847
|
}
|
|
402
848
|
return pending;
|
|
403
849
|
}
|
|
850
|
+
#rememberOversizedContinuation(cursorId) {
|
|
851
|
+
this.#oversizedContinuations.add(cursorId);
|
|
852
|
+
if (this.#oversizedContinuations.size <= maxSourceFiles)
|
|
853
|
+
return;
|
|
854
|
+
const oldest = this.#oversizedContinuations.values().next().value;
|
|
855
|
+
if (oldest !== undefined)
|
|
856
|
+
this.#oversizedContinuations.delete(oldest);
|
|
857
|
+
}
|
|
404
858
|
#setCursor(cursorId, legacyCursorId, offset) {
|
|
405
859
|
this.#cursors?.set(cursorId, offset);
|
|
406
860
|
if (cursorId !== legacyCursorId) {
|
|
@@ -417,7 +871,11 @@ export class JSONLSourceReader {
|
|
|
417
871
|
return;
|
|
418
872
|
if (this.#cursors?.has(fileCursorId(fileReal, metadata)) || this.#cursors?.has(digest(fileReal)))
|
|
419
873
|
return;
|
|
420
|
-
|
|
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);
|
|
421
879
|
}
|
|
422
880
|
}
|
|
423
881
|
export function expandedActivityRoot(root) {
|
|
@@ -470,7 +928,16 @@ async function listJSONLFiles(root) {
|
|
|
470
928
|
basename(entry.name).toLowerCase().endsWith(".jsonl")) {
|
|
471
929
|
try {
|
|
472
930
|
const metadata = await stat(path);
|
|
473
|
-
result.push({
|
|
931
|
+
result.push({
|
|
932
|
+
path,
|
|
933
|
+
modifiedMs: metadata.mtimeMs,
|
|
934
|
+
identity: {
|
|
935
|
+
size: metadata.size,
|
|
936
|
+
dev: metadata.dev,
|
|
937
|
+
ino: metadata.ino,
|
|
938
|
+
birthtimeMs: metadata.birthtimeMs,
|
|
939
|
+
},
|
|
940
|
+
});
|
|
474
941
|
if (result.length > maxCursorEntries * 2) {
|
|
475
942
|
result.sort(recentFileFirst);
|
|
476
943
|
result.length = maxCursorEntries;
|
|
@@ -491,6 +958,17 @@ async function listJSONLFiles(root) {
|
|
|
491
958
|
await walk(root, 0);
|
|
492
959
|
return result.sort(recentFileFirst).slice(0, maxCursorEntries);
|
|
493
960
|
}
|
|
961
|
+
async function readRange(path, position, length) {
|
|
962
|
+
const handle = await open(path, "r");
|
|
963
|
+
try {
|
|
964
|
+
const content = Buffer.alloc(length);
|
|
965
|
+
const read = await handle.read(content, 0, length, position);
|
|
966
|
+
return content.subarray(0, read.bytesRead);
|
|
967
|
+
}
|
|
968
|
+
finally {
|
|
969
|
+
await handle.close();
|
|
970
|
+
}
|
|
971
|
+
}
|
|
494
972
|
function recentFileFirst(left, right) {
|
|
495
973
|
return (right.modifiedMs - left.modifiedMs || left.path.localeCompare(right.path));
|
|
496
974
|
}
|
|
@@ -546,6 +1024,9 @@ function reconciledSourceFile(claims) {
|
|
|
546
1024
|
root: representative.root,
|
|
547
1025
|
path: representative.path,
|
|
548
1026
|
modifiedMs: Math.max(...ordered.map((claim) => claim.modifiedMs)),
|
|
1027
|
+
...(representative.identity === undefined
|
|
1028
|
+
? {}
|
|
1029
|
+
: { identity: representative.identity }),
|
|
549
1030
|
};
|
|
550
1031
|
}
|
|
551
1032
|
function compareSourceClaim(left, right) {
|
|
@@ -553,6 +1034,24 @@ function compareSourceClaim(left, right) {
|
|
|
553
1034
|
(left.sourceDisplayName ?? "").localeCompare(right.sourceDisplayName ?? "") ||
|
|
554
1035
|
left.root.localeCompare(right.root);
|
|
555
1036
|
}
|
|
1037
|
+
function fileIdentityKey(identity) {
|
|
1038
|
+
if (identity === undefined || (identity.ino <= 0 && identity.dev <= 0)) {
|
|
1039
|
+
return undefined;
|
|
1040
|
+
}
|
|
1041
|
+
return `${identity.dev}:${identity.ino}:${identity.birthtimeMs}`;
|
|
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
|
+
}
|
|
556
1055
|
function fileCursorId(path, metadata) {
|
|
557
1056
|
const identity = metadata.ino > 0 || metadata.dev > 0
|
|
558
1057
|
? `inode:${metadata.dev}:${metadata.ino}:${metadata.birthtimeMs}`
|
|
@@ -573,6 +1072,24 @@ function newestFilesPerAdapter(files, limit) {
|
|
|
573
1072
|
}
|
|
574
1073
|
return selected;
|
|
575
1074
|
}
|
|
1075
|
+
function configuredRoots(sources, format) {
|
|
1076
|
+
return new Set(sources.flatMap((source) => source.format === format ? source.roots.map(expandedActivityRoot) : []));
|
|
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
|
+
}
|
|
1084
|
+
function sameCursors(left, right) {
|
|
1085
|
+
if (left.size !== right.size)
|
|
1086
|
+
return false;
|
|
1087
|
+
for (const [key, offset] of left) {
|
|
1088
|
+
if (right.get(key) !== offset)
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1091
|
+
return true;
|
|
1092
|
+
}
|
|
576
1093
|
function isWithin(root, candidate) {
|
|
577
1094
|
const path = relative(resolve(root), resolve(candidate));
|
|
578
1095
|
return path === "" || (!path.startsWith(`..${sep}`) && path !== "..");
|
|
@@ -589,6 +1106,7 @@ function isTransientSourceError(error) {
|
|
|
589
1106
|
error.code === "EACCES" ||
|
|
590
1107
|
error.code === "EPERM" ||
|
|
591
1108
|
error.code === "ENOTDIR" ||
|
|
1109
|
+
error.code === "EBUSY" ||
|
|
592
1110
|
error.code === "ESTALE"));
|
|
593
1111
|
}
|
|
594
1112
|
//# sourceMappingURL=source-reader.js.map
|