@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
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdir, stat, unlink } from "node:fs/promises";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { create, toBinary } from "@bufbuild/protobuf";
|
|
4
5
|
import { timestampFromDate } from "@bufbuild/protobuf/wkt";
|
|
6
|
+
import { abortableDelay } from "../abortable-delay.js";
|
|
5
7
|
import { ActivityKind, AgentKind, AgentState, } from "../generated/phigon/v1/common_pb.js";
|
|
6
8
|
import { AgentIdentitySchema, } from "../generated/phigon/v1/identity_pb.js";
|
|
7
9
|
import { mergeInventoryWithHistoricalIdentities } from "../historical-identity-store.js";
|
|
@@ -27,10 +29,19 @@ import { genericActivityAdapter } from "./adapters/generic.js";
|
|
|
27
29
|
import { LocalPolicyEngine } from "./policy.js";
|
|
28
30
|
import { vendorDraft } from "./normalize.js";
|
|
29
31
|
import { objectValue, recordObservedAt, stringField, } from "./record.js";
|
|
30
|
-
import { JSONLSourceReader, } from "./source-reader.js";
|
|
32
|
+
import { JSONLSourceReader, fileReadThrough, } from "./source-reader.js";
|
|
33
|
+
import { bridgeWritingSuffix, ingestedBridgeFile } from "./ingest.js";
|
|
34
|
+
import { closedJournalSegments } from "../bridge-journal.js";
|
|
31
35
|
import { processKeyCapability } from "../discovery/process-identity.js";
|
|
32
36
|
const maxEnvelopeBytes = 1 << 20;
|
|
33
37
|
const maxPendingCheckpoints = 256;
|
|
38
|
+
// Bridge files are cleaned up this often, and kept at most this long.
|
|
39
|
+
const bridgePruneIntervalMs = 5 * 60_000;
|
|
40
|
+
const bridgeFileRetentionMs = 7 * 86_400_000;
|
|
41
|
+
// A closed journal segment untouched this long has no writer left.
|
|
42
|
+
const closedSegmentQuietMs = 60_000;
|
|
43
|
+
// Saved live checkpoints remembered while their events await acknowledgement.
|
|
44
|
+
const retainedLiveCheckpoints = 8;
|
|
34
45
|
export const historySyncQueueHeadroom = 1_024;
|
|
35
46
|
const maxDraftsPerRecord = 32;
|
|
36
47
|
const adapters = new Map([
|
|
@@ -46,6 +57,10 @@ const adapters = new Map([
|
|
|
46
57
|
// Keep the flexible generic parser after the generated bridge adapters so
|
|
47
58
|
// the generic bridge cannot accidentally replace it with a fixed label.
|
|
48
59
|
["generic", genericActivityAdapter],
|
|
60
|
+
// Deliberately replaces the generated OpenCode bridge parser: the native
|
|
61
|
+
// adapter reads both OpenCode's database events and the structured CLI
|
|
62
|
+
// records (step_start, tool_use, text, step_finish) that
|
|
63
|
+
// `ingest --agent opencode` receives, as docs/activity-collection.md says.
|
|
49
64
|
["opencode", openCodeActivityAdapter],
|
|
50
65
|
// Native recordings use their own adapter IDs so the OpenAI-compatible
|
|
51
66
|
// `ingest --agent` bridges for the same products keep their parser.
|
|
@@ -61,6 +76,18 @@ const adapters = new Map([
|
|
|
61
76
|
["roo-code-task", rooCodeActivityAdapter],
|
|
62
77
|
["kilo-code-task", kiloCodeActivityAdapter],
|
|
63
78
|
]);
|
|
79
|
+
/**
|
|
80
|
+
* `emit` refused an event. The reader was put back to where the scan
|
|
81
|
+
* started, so its records are read again once delivery has room.
|
|
82
|
+
*/
|
|
83
|
+
class DeliveryRefusedError extends Error {
|
|
84
|
+
}
|
|
85
|
+
const sourceIdentityFailureKey = "agent identities";
|
|
86
|
+
/** The kind of an error, never its message, which may quote a record. */
|
|
87
|
+
function errorName(error) {
|
|
88
|
+
const name = error instanceof Error ? error.name : "";
|
|
89
|
+
return /^[A-Za-z][A-Za-z0-9]{0,63}$/u.test(name) ? name : "unknown error";
|
|
90
|
+
}
|
|
64
91
|
export class ActivityCollector {
|
|
65
92
|
#config;
|
|
66
93
|
#credential;
|
|
@@ -74,6 +101,15 @@ export class ActivityCollector {
|
|
|
74
101
|
#canCollect;
|
|
75
102
|
#canSyncHistory;
|
|
76
103
|
#onSourceIdentity;
|
|
104
|
+
#onDeliveryRefused;
|
|
105
|
+
#onSourceFailure;
|
|
106
|
+
#reportedFailures = new WeakSet();
|
|
107
|
+
// Failures in a row of record conversion, per adapter, and of saving
|
|
108
|
+
// source identities. The keys are a fixed, small set.
|
|
109
|
+
#collectionFailures = new Map();
|
|
110
|
+
#registeredSourcesFailing = false;
|
|
111
|
+
#deliveryRefused = false;
|
|
112
|
+
#refusedDeliveries = 0;
|
|
77
113
|
#policy;
|
|
78
114
|
#limiter;
|
|
79
115
|
#agentsByKind = new Map();
|
|
@@ -87,6 +123,11 @@ export class ActivityCollector {
|
|
|
87
123
|
#oversizedEvents = 0;
|
|
88
124
|
#acknowledgedSequence;
|
|
89
125
|
#durableDelivery;
|
|
126
|
+
// Saved live-reader checkpoints, oldest first, with the sequence that
|
|
127
|
+
// their events end at. Bridge files they read through can be deleted once
|
|
128
|
+
// the server acknowledges that sequence.
|
|
129
|
+
#committedLive = [];
|
|
130
|
+
#flushDelivery;
|
|
90
131
|
#pendingCheckpoints = [];
|
|
91
132
|
#checkpointSave = Promise.resolve();
|
|
92
133
|
#sourceIdentitySaves = [];
|
|
@@ -106,6 +147,8 @@ export class ActivityCollector {
|
|
|
106
147
|
this.#canCollect = options.canCollect ?? (() => true);
|
|
107
148
|
this.#canSyncHistory = options.canSyncHistory ?? this.#canCollect;
|
|
108
149
|
this.#onSourceIdentity = options.onSourceIdentity ?? (() => undefined);
|
|
150
|
+
this.#onDeliveryRefused = options.onDeliveryRefused ?? (() => undefined);
|
|
151
|
+
this.#onSourceFailure = options.onSourceFailure ?? (() => undefined);
|
|
109
152
|
this.#policy = new LocalPolicyEngine(options.policy ?? options.config.policy, options.config.redactionPathPatterns);
|
|
110
153
|
this.#limiter = new MinuteRateLimiter(options.config.activityRateLimitPerMinute);
|
|
111
154
|
this.#lastSequence = options.initialSequence;
|
|
@@ -116,6 +159,7 @@ export class ActivityCollector {
|
|
|
116
159
|
throw new Error("Initial activity acknowledgement is invalid.");
|
|
117
160
|
}
|
|
118
161
|
this.#durableDelivery = options.durableDelivery ?? false;
|
|
162
|
+
this.#flushDelivery = options.flushDelivery ?? (() => undefined);
|
|
119
163
|
}
|
|
120
164
|
updateInventory(inventory) {
|
|
121
165
|
this.#agentsByKind.clear();
|
|
@@ -129,6 +173,9 @@ export class ActivityCollector {
|
|
|
129
173
|
if (acknowledgedSequence < 0n || allocationSequence < 0n) {
|
|
130
174
|
throw new Error("Activity sequence must not be negative.");
|
|
131
175
|
}
|
|
176
|
+
// The server lost acknowledgements: no saved checkpoint is known to be
|
|
177
|
+
// delivered any more.
|
|
178
|
+
this.#committedLive = [];
|
|
132
179
|
this.#acknowledgedSequence = acknowledgedSequence;
|
|
133
180
|
this.#lastSequence =
|
|
134
181
|
allocationSequence < acknowledgedSequence
|
|
@@ -162,26 +209,35 @@ export class ActivityCollector {
|
|
|
162
209
|
throw new Error("History sync expired before all available records were uploaded.");
|
|
163
210
|
}
|
|
164
211
|
if (!this.#canSyncHistory()) {
|
|
165
|
-
await abortableDelay(250, signal
|
|
212
|
+
await abortableDelay(250, signal);
|
|
166
213
|
continue;
|
|
167
214
|
}
|
|
168
215
|
// Release the collection lock between bounded batches. A large archive
|
|
169
216
|
// must not prevent the live reader from following current work.
|
|
170
217
|
const started = performance.now();
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
218
|
+
let pass;
|
|
219
|
+
try {
|
|
220
|
+
pass = await this.#withScanLock(async () => {
|
|
221
|
+
const before = this.#emittedEvents;
|
|
222
|
+
const result = await this.#collectOnce(this.#historyReader, true);
|
|
223
|
+
eventsUploaded += this.#emittedEvents - before;
|
|
224
|
+
return result;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
if (!(error instanceof DeliveryRefusedError))
|
|
229
|
+
throw error;
|
|
230
|
+
await abortableDelay(250, signal);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
177
233
|
recordsScanned += pass.sourceRecords;
|
|
178
234
|
filesDiscovered = Math.max(filesDiscovered, pass.discoveredFiles);
|
|
179
235
|
complete = pass.complete;
|
|
180
236
|
if (!complete && pass.sourceRecords === 0) {
|
|
181
|
-
await abortableDelay(100, signal
|
|
237
|
+
await abortableDelay(100, signal);
|
|
182
238
|
}
|
|
183
239
|
else if (!complete) {
|
|
184
|
-
await abortableDelay(historyRest(performance.now() - started), signal
|
|
240
|
+
await abortableDelay(historyRest(performance.now() - started), signal);
|
|
185
241
|
}
|
|
186
242
|
}
|
|
187
243
|
return Object.freeze({
|
|
@@ -212,10 +268,12 @@ export class ActivityCollector {
|
|
|
212
268
|
discoveredFiles: 0,
|
|
213
269
|
};
|
|
214
270
|
}
|
|
271
|
+
const start = reader.position();
|
|
215
272
|
const scan = await reader.scan([
|
|
216
273
|
...this.#sources,
|
|
217
|
-
...await this.#
|
|
274
|
+
...await this.#registeredSources(),
|
|
218
275
|
], Math.max(1, Math.floor(allowance / maxDraftsPerRecord)));
|
|
276
|
+
this.#reportSourceFailures(reader);
|
|
219
277
|
this.#sourceRecords += scan.metrics.records;
|
|
220
278
|
this.#malformedRecords += scan.metrics.malformed;
|
|
221
279
|
this.#oversizedRecords += scan.metrics.oversized;
|
|
@@ -224,61 +282,47 @@ export class ActivityCollector {
|
|
|
224
282
|
const adapter = adapters.get(record.adapterId);
|
|
225
283
|
if (adapter === undefined)
|
|
226
284
|
continue;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const nativeDraft = vendorDraft(adapter.agentKind, context, genericAgent?.hint !== undefined
|
|
242
|
-
? genericAgent.hint
|
|
243
|
-
: adapter.id, vendorType(record.value), record.value);
|
|
244
|
-
const candidateDrafts = [nativeDraft, ...drafts.slice(0, 31)];
|
|
245
|
-
const permittedDrafts = !historical && record.initialReplay === true &&
|
|
246
|
-
this.#config.activityInitialReplayMode === "messages"
|
|
247
|
-
? candidateDrafts.filter((draft) => draft.payload.case === "activity" &&
|
|
248
|
-
(draft.payload.value.kind === ActivityKind.PROMPT ||
|
|
249
|
-
draft.payload.value.kind === ActivityKind.RESPONSE))
|
|
250
|
-
: candidateDrafts;
|
|
285
|
+
let converted;
|
|
286
|
+
try {
|
|
287
|
+
converted = this.#convertRecord(record, adapter, historical, now);
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
// A record this observer cannot convert is skipped like a malformed
|
|
291
|
+
// one: reading it again would fail the same way, and stopping
|
|
292
|
+
// would end collection from every source.
|
|
293
|
+
this.#malformedRecords++;
|
|
294
|
+
this.#reportCollectionFailure(record.adapterId, `one record could not be converted and was skipped (${errorName(error)})`);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
this.#collectionFailures.delete(record.adapterId);
|
|
298
|
+
this.#oversizedEvents += converted.oversized;
|
|
251
299
|
// A source record is atomic: never advance its cursor after dropping
|
|
252
300
|
// part of its normalized activity. One record may use the final budget
|
|
253
301
|
// plus a bounded burst; the next scan waits for replenishment.
|
|
254
302
|
if (!historical)
|
|
255
|
-
this.#limiter.consume(now,
|
|
256
|
-
for (const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
tenantId: this.#credential.tenantId,
|
|
260
|
-
machineId: this.#credential.machineId,
|
|
261
|
-
agentId: this.#agentID(draft.agentKind, record.adapterId, record.sourceId, record.value, record.observedAt, genericAgent?.displayName),
|
|
262
|
-
sessionId: draft.sessionId,
|
|
263
|
-
sequence: nextSequence,
|
|
264
|
-
observedAt: timestampFromDate(clampObservedAt(draft.observedAt, now)),
|
|
265
|
-
schemaVersion: 1,
|
|
266
|
-
privacyClass: draft.privacyClass,
|
|
267
|
-
sourceEventId: createHash("sha256")
|
|
268
|
-
.update(`${record.sourceRecordId}\u0000${draftIndex}\u0000${draft.payload.case}`)
|
|
269
|
-
.digest("hex"),
|
|
270
|
-
payload: draft.payload,
|
|
271
|
-
});
|
|
272
|
-
if (toBinary(EventEnvelopeSchema, event).byteLength >
|
|
273
|
-
maxEnvelopeBytes) {
|
|
274
|
-
this.#oversizedEvents++;
|
|
275
|
-
continue;
|
|
303
|
+
this.#limiter.consume(now, converted.drafts);
|
|
304
|
+
for (const event of converted.events) {
|
|
305
|
+
try {
|
|
306
|
+
this.#emit(event);
|
|
276
307
|
}
|
|
277
|
-
|
|
278
|
-
|
|
308
|
+
catch (error) {
|
|
309
|
+
// The reader already moved past this scan's records. Put it back
|
|
310
|
+
// so none is skipped; the ones delivered before this event are
|
|
311
|
+
// sent again with the same source event IDs, which the server
|
|
312
|
+
// deduplicates. The sequence stays unused, leaving no gap.
|
|
313
|
+
reader.rewind(start);
|
|
314
|
+
this.#refusedDeliveries++;
|
|
315
|
+
if (!this.#deliveryRefused) {
|
|
316
|
+
this.#deliveryRefused = true;
|
|
317
|
+
this.#onDeliveryRefused(error);
|
|
318
|
+
}
|
|
319
|
+
throw new DeliveryRefusedError("Telemetry delivery refused an activity event.", { cause: error });
|
|
320
|
+
}
|
|
321
|
+
this.#lastSequence = event.sequence;
|
|
279
322
|
this.#emittedEvents++;
|
|
280
323
|
}
|
|
281
324
|
}
|
|
325
|
+
this.#deliveryRefused = false;
|
|
282
326
|
await this.#flushSourceIdentitySaves();
|
|
283
327
|
const pending = {
|
|
284
328
|
throughSequence: this.#lastSequence,
|
|
@@ -300,10 +344,99 @@ export class ActivityCollector {
|
|
|
300
344
|
discoveredFiles: scan.metrics.discoveredFiles,
|
|
301
345
|
};
|
|
302
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* The events for one source record, numbered from the next sequence, and
|
|
349
|
+
* how many normalized drafts they came from. Throws when the record cannot
|
|
350
|
+
* be converted; nothing is emitted for it then.
|
|
351
|
+
*/
|
|
352
|
+
#convertRecord(record, adapter, historical, now) {
|
|
353
|
+
const sessionID = reportedSessionID(record.adapterId, record.value, record.sourceId);
|
|
354
|
+
const drafts = adapter.parse(record.value, {
|
|
355
|
+
sessionId: sessionID,
|
|
356
|
+
observedAt: record.observedAt,
|
|
357
|
+
policy: this.#policy,
|
|
358
|
+
});
|
|
359
|
+
const context = {
|
|
360
|
+
sessionId: sessionID,
|
|
361
|
+
observedAt: recordObservedAt(record.value, record.observedAt),
|
|
362
|
+
policy: this.#policy,
|
|
363
|
+
};
|
|
364
|
+
const genericAgent = record.adapterId === "generic"
|
|
365
|
+
? reportedGenericAgentIdentity(record.value, record.sourceDisplayName)
|
|
366
|
+
: undefined;
|
|
367
|
+
const nativeDraft = vendorDraft(adapter.agentKind, context, genericAgent?.hint !== undefined
|
|
368
|
+
? genericAgent.hint
|
|
369
|
+
: adapter.id, vendorType(record.value), record.value);
|
|
370
|
+
const candidateDrafts = [nativeDraft, ...drafts.slice(0, 31)];
|
|
371
|
+
const permittedDrafts = !historical && record.initialReplay === true &&
|
|
372
|
+
this.#config.activityInitialReplayMode === "messages"
|
|
373
|
+
? candidateDrafts.filter((draft) => draft.payload.case === "activity" &&
|
|
374
|
+
(draft.payload.value.kind === ActivityKind.PROMPT ||
|
|
375
|
+
draft.payload.value.kind === ActivityKind.RESPONSE))
|
|
376
|
+
: candidateDrafts;
|
|
377
|
+
const events = [];
|
|
378
|
+
let oversized = 0;
|
|
379
|
+
for (const [draftIndex, draft] of permittedDrafts.entries()) {
|
|
380
|
+
const event = create(EventEnvelopeSchema, {
|
|
381
|
+
tenantId: this.#credential.tenantId,
|
|
382
|
+
machineId: this.#credential.machineId,
|
|
383
|
+
agentId: this.#agentID(draft.agentKind, record.adapterId, record.sourceId, record.value, record.observedAt, genericAgent?.displayName),
|
|
384
|
+
sessionId: draft.sessionId,
|
|
385
|
+
sequence: this.#lastSequence + 1n + BigInt(events.length),
|
|
386
|
+
observedAt: timestampFromDate(clampObservedAt(draft.observedAt, now)),
|
|
387
|
+
schemaVersion: 1,
|
|
388
|
+
privacyClass: draft.privacyClass,
|
|
389
|
+
sourceEventId: createHash("sha256")
|
|
390
|
+
.update(`${record.sourceRecordId}\u0000${draftIndex}\u0000${draft.payload.case}`)
|
|
391
|
+
.digest("hex"),
|
|
392
|
+
payload: draft.payload,
|
|
393
|
+
});
|
|
394
|
+
if (toBinary(EventEnvelopeSchema, event).byteLength >
|
|
395
|
+
maxEnvelopeBytes) {
|
|
396
|
+
oversized++;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
events.push(event);
|
|
400
|
+
}
|
|
401
|
+
return { events, drafts: permittedDrafts.length, oversized };
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Says that collection work keeps failing, at the 1st, 2nd, 4th, 8th...
|
|
405
|
+
* failure in a row, so it stays visible without flooding the log. The
|
|
406
|
+
* message never includes activity content.
|
|
407
|
+
*/
|
|
408
|
+
#reportCollectionFailure(key, message) {
|
|
409
|
+
const failures = (this.#collectionFailures.get(key) ?? 0) + 1;
|
|
410
|
+
this.#collectionFailures.set(key, failures);
|
|
411
|
+
if ((failures & (failures - 1)) !== 0)
|
|
412
|
+
return;
|
|
413
|
+
const now = this.#clock();
|
|
414
|
+
this.#onSourceFailure(Object.freeze({
|
|
415
|
+
adapterId: key,
|
|
416
|
+
failures,
|
|
417
|
+
message,
|
|
418
|
+
failedAt: now,
|
|
419
|
+
retryAt: now,
|
|
420
|
+
}));
|
|
421
|
+
}
|
|
303
422
|
async run(signal) {
|
|
423
|
+
let prunedAt = 0;
|
|
304
424
|
try {
|
|
305
425
|
while (!signal.aborted) {
|
|
306
|
-
|
|
426
|
+
try {
|
|
427
|
+
await this.scanOnce();
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
// A full queue or spool waits for acknowledgements like any
|
|
431
|
+
// other backpressure; the refused records are read again.
|
|
432
|
+
if (!(error instanceof DeliveryRefusedError))
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
if (this.#clock().getTime() - prunedAt >= bridgePruneIntervalMs) {
|
|
436
|
+
prunedAt = this.#clock().getTime();
|
|
437
|
+
// Cleanup only frees disk space; it retries on the next pass.
|
|
438
|
+
await this.pruneBridgeFiles().catch(() => undefined);
|
|
439
|
+
}
|
|
307
440
|
await abortableDelay(this.#config.activityScanIntervalMs, signal);
|
|
308
441
|
}
|
|
309
442
|
}
|
|
@@ -311,6 +444,56 @@ export class ActivityCollector {
|
|
|
311
444
|
this.close();
|
|
312
445
|
}
|
|
313
446
|
}
|
|
447
|
+
/**
|
|
448
|
+
* Deletes bridge files this observer is done with: an `ingest` file or a
|
|
449
|
+
* closed journal segment whose records were all read under a checkpoint
|
|
450
|
+
* the server has acknowledged through, or any of them after seven days.
|
|
451
|
+
* A file an `ingest` run is still writing, and each journal's newest
|
|
452
|
+
* segment, are never deleted while in use.
|
|
453
|
+
*/
|
|
454
|
+
async pruneBridgeFiles() {
|
|
455
|
+
const now = this.#clock().getTime();
|
|
456
|
+
const acknowledged = this.#acknowledgedSequence;
|
|
457
|
+
const settled = this.#committedLive.findLast((checkpoint) => checkpoint.throughSequence <= acknowledged);
|
|
458
|
+
for (const agent of activityBridgeAgents) {
|
|
459
|
+
const root = join(this.#config.dataDirectory, "bridges", agent.id);
|
|
460
|
+
let names;
|
|
461
|
+
try {
|
|
462
|
+
names = await readdir(root);
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const present = new Set(names);
|
|
468
|
+
const closedSegments = new Set(closedJournalSegments(names));
|
|
469
|
+
for (const name of names) {
|
|
470
|
+
const ingested = ingestedBridgeFile.test(name);
|
|
471
|
+
if (!ingested && !closedSegments.has(name))
|
|
472
|
+
continue;
|
|
473
|
+
const path = join(root, name);
|
|
474
|
+
try {
|
|
475
|
+
const modifiedMs = (await stat(path)).mtimeMs;
|
|
476
|
+
const expired = now - modifiedMs >= bridgeFileRetentionMs;
|
|
477
|
+
const inUse = ingested
|
|
478
|
+
? present.has(`${name}${bridgeWritingSuffix}`)
|
|
479
|
+
// A segment closes when its journal starts the next one; a
|
|
480
|
+
// writer that chose it just before may still be appending.
|
|
481
|
+
: now - modifiedMs < closedSegmentQuietMs;
|
|
482
|
+
if (expired ||
|
|
483
|
+
(!inUse && settled !== undefined &&
|
|
484
|
+
await fileReadThrough(path, settled.cursors))) {
|
|
485
|
+
await unlink(path);
|
|
486
|
+
if (ingested) {
|
|
487
|
+
await unlink(`${path}${bridgeWritingSuffix}`).catch(() => undefined);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
// Gone, or locked by another program: retried on the next pass.
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
314
497
|
/**
|
|
315
498
|
* Release the database connections the live reader keeps open. A later
|
|
316
499
|
* scan opens them again.
|
|
@@ -329,8 +512,53 @@ export class ActivityCollector {
|
|
|
329
512
|
lastSequence: this.#lastSequence,
|
|
330
513
|
pendingCheckpoints: this.#pendingCheckpoints.length,
|
|
331
514
|
backpressuredScans: this.#backpressuredScans,
|
|
515
|
+
refusedDeliveries: this.#refusedDeliveries,
|
|
516
|
+
failingSources: this.#reader.sourceFailures().length,
|
|
332
517
|
});
|
|
333
518
|
}
|
|
519
|
+
/**
|
|
520
|
+
* Sources of registered custom agents. When their list cannot be read,
|
|
521
|
+
* this scan goes on with the built-in sources.
|
|
522
|
+
*/
|
|
523
|
+
async #registeredSources() {
|
|
524
|
+
try {
|
|
525
|
+
const sources = await this.#additionalSources();
|
|
526
|
+
this.#registeredSourcesFailing = false;
|
|
527
|
+
return sources;
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
if (!this.#registeredSourcesFailing) {
|
|
531
|
+
this.#registeredSourcesFailing = true;
|
|
532
|
+
const now = this.#clock();
|
|
533
|
+
this.#onSourceFailure(Object.freeze({
|
|
534
|
+
adapterId: "registered agents",
|
|
535
|
+
failures: 1,
|
|
536
|
+
message: (error instanceof Error ? error.message : String(error))
|
|
537
|
+
.replace(/\s+/gu, " ")
|
|
538
|
+
.slice(0, 200),
|
|
539
|
+
failedAt: now,
|
|
540
|
+
// Read again on the next scan.
|
|
541
|
+
retryAt: now,
|
|
542
|
+
}));
|
|
543
|
+
}
|
|
544
|
+
return [];
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
#reportSourceFailures(reader) {
|
|
548
|
+
for (const failure of reader.sourceFailures()) {
|
|
549
|
+
if (failure.failures !== 1 || this.#reportedFailures.has(failure))
|
|
550
|
+
continue;
|
|
551
|
+
this.#reportedFailures.add(failure);
|
|
552
|
+
this.#onSourceFailure(failure);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/** Sources and files skipped after a failure, by the live and history readers. */
|
|
556
|
+
sourceFailures() {
|
|
557
|
+
return [
|
|
558
|
+
...this.#reader.sourceFailures(),
|
|
559
|
+
...this.#historyReader.sourceFailures(),
|
|
560
|
+
];
|
|
561
|
+
}
|
|
334
562
|
sourceIdentities() {
|
|
335
563
|
return [...this.#sourceIdentities.values()].sort((left, right) => left.agentId.localeCompare(right.agentId));
|
|
336
564
|
}
|
|
@@ -344,15 +572,34 @@ export class ActivityCollector {
|
|
|
344
572
|
this.#checkpointSave = this.#checkpointSave
|
|
345
573
|
.catch(() => undefined)
|
|
346
574
|
.then(async () => {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
575
|
+
let currentDurableThrough = this.#acknowledgedSequence;
|
|
576
|
+
if (this.#durableDelivery) {
|
|
577
|
+
const emittedThrough = this.#lastSequence;
|
|
578
|
+
try {
|
|
579
|
+
await this.#flushDelivery();
|
|
580
|
+
currentDurableThrough = emittedThrough;
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
583
|
+
// Not written: only checkpoints the server acknowledged move.
|
|
584
|
+
// The rest stay pending for the next save, and delivery refuses
|
|
585
|
+
// new events until the spool can be written again.
|
|
586
|
+
}
|
|
587
|
+
}
|
|
350
588
|
const committed = new Set();
|
|
351
589
|
for (const pending of eligible) {
|
|
352
590
|
if (pending.throughSequence > currentDurableThrough)
|
|
353
591
|
continue;
|
|
354
592
|
await pending.reader.commit(pending.checkpoint);
|
|
355
593
|
committed.add(pending);
|
|
594
|
+
if (pending.reader === this.#reader) {
|
|
595
|
+
this.#committedLive = [
|
|
596
|
+
...this.#committedLive.slice(-(retainedLiveCheckpoints - 1)),
|
|
597
|
+
{
|
|
598
|
+
cursors: pending.checkpoint.cursors,
|
|
599
|
+
throughSequence: pending.throughSequence,
|
|
600
|
+
},
|
|
601
|
+
];
|
|
602
|
+
}
|
|
356
603
|
}
|
|
357
604
|
this.#pendingCheckpoints = this.#pendingCheckpoints.filter((candidate) => !committed.has(candidate));
|
|
358
605
|
});
|
|
@@ -404,21 +651,45 @@ export class ActivityCollector {
|
|
|
404
651
|
capabilities: ["activity", "source-isolated", "unattributed"],
|
|
405
652
|
});
|
|
406
653
|
this.#sourceIdentities.set(agentID, identity);
|
|
654
|
+
// An identity that could not be saved is forgotten, so the source's
|
|
655
|
+
// next record saves it again.
|
|
656
|
+
const forget = (error) => {
|
|
657
|
+
if (this.#sourceIdentities.get(agentID) === identity) {
|
|
658
|
+
this.#sourceIdentities.delete(agentID);
|
|
659
|
+
}
|
|
660
|
+
throw error;
|
|
661
|
+
};
|
|
662
|
+
let save;
|
|
407
663
|
try {
|
|
408
|
-
|
|
664
|
+
save = Promise.resolve(this.#onSourceIdentity(identity)).catch(forget);
|
|
409
665
|
}
|
|
410
666
|
catch (error) {
|
|
411
|
-
|
|
667
|
+
save = Promise.reject(error).catch(forget);
|
|
412
668
|
}
|
|
669
|
+
// Handled here too: a scan that stops early leaves the save for the
|
|
670
|
+
// next flush, and an unhandled rejection would end the process.
|
|
671
|
+
save.catch(() => undefined);
|
|
672
|
+
this.#sourceIdentitySaves.push(save);
|
|
413
673
|
}
|
|
674
|
+
/**
|
|
675
|
+
* Waits for the identity saves this scan started. A failed save is
|
|
676
|
+
* reported and retried with a later record; it never stops collection,
|
|
677
|
+
* because the events already carry the agent ID.
|
|
678
|
+
*/
|
|
414
679
|
async #flushSourceIdentitySaves() {
|
|
415
680
|
const pending = this.#sourceIdentitySaves.splice(0);
|
|
416
681
|
if (pending.length === 0)
|
|
417
682
|
return;
|
|
418
683
|
const results = await Promise.allSettled(pending);
|
|
419
684
|
const failure = results.find((result) => result.status === "rejected");
|
|
420
|
-
if (failure
|
|
421
|
-
|
|
685
|
+
if (failure === undefined) {
|
|
686
|
+
this.#collectionFailures.delete(sourceIdentityFailureKey);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const reason = failure.reason;
|
|
690
|
+
this.#reportCollectionFailure(sourceIdentityFailureKey, (reason instanceof Error ? reason.message : String(reason))
|
|
691
|
+
.replace(/\s+/gu, " ")
|
|
692
|
+
.slice(0, 200));
|
|
422
693
|
}
|
|
423
694
|
}
|
|
424
695
|
export function mergeInventoryWithSourceIdentities(inventory, sourceIdentities) {
|
|
@@ -845,15 +1116,4 @@ export function historyRest(spentMs) {
|
|
|
845
1116
|
return 0;
|
|
846
1117
|
return Math.min(1_000, Math.round(spentMs));
|
|
847
1118
|
}
|
|
848
|
-
async function abortableDelay(milliseconds, signal) {
|
|
849
|
-
if (signal.aborted)
|
|
850
|
-
return;
|
|
851
|
-
await new Promise((resolve) => {
|
|
852
|
-
const timeout = setTimeout(resolve, milliseconds);
|
|
853
|
-
signal.addEventListener("abort", () => {
|
|
854
|
-
clearTimeout(timeout);
|
|
855
|
-
resolve();
|
|
856
|
-
}, { once: true });
|
|
857
|
-
});
|
|
858
|
-
}
|
|
859
1119
|
//# sourceMappingURL=collector.js.map
|