@pdpp/local-collector 0.1.0-beta.7 → 0.1.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/local-collector/bin/pdpp-local-collector.js +580 -22
- package/dist/local-collector/src/runner.d.ts +1 -1
- package/dist/local-collector/src/runner.js +15 -1
- package/dist/polyfill-connectors/connectors/claude_code/index.js +60 -37
- package/dist/polyfill-connectors/connectors/codex/index.js +390 -108
- package/dist/polyfill-connectors/connectors/codex/parsers.js +5 -3
- package/dist/polyfill-connectors/src/bounded-file-preview.js +76 -0
- package/dist/polyfill-connectors/src/browser-handoff.js +38 -5
- package/dist/polyfill-connectors/src/collector-build-info.d.ts +8 -0
- package/dist/polyfill-connectors/src/collector-build-info.js +10 -0
- package/dist/polyfill-connectors/src/collector-runner.d.ts +54 -0
- package/dist/polyfill-connectors/src/collector-runner.js +250 -18
- package/dist/polyfill-connectors/src/connector-exit.js +62 -0
- package/dist/polyfill-connectors/src/connector-runtime-protocol.d.ts +41 -21
- package/dist/polyfill-connectors/src/connector-runtime.js +241 -30
- package/dist/polyfill-connectors/src/fingerprint-cursor.js +107 -0
- package/dist/polyfill-connectors/src/local-device-client.d.ts +17 -0
- package/dist/polyfill-connectors/src/local-device-client.js +69 -9
- package/dist/polyfill-connectors/src/local-device-outbox.d.ts +59 -0
- package/dist/polyfill-connectors/src/local-device-outbox.js +394 -5
- package/dist/polyfill-connectors/src/local-source-inventory.js +8 -1
- package/dist/polyfill-connectors/src/runner/index.d.ts +4 -3
- package/dist/polyfill-connectors/src/runner/index.js +4 -3
- package/dist/polyfill-connectors/src/safe-text-preview.js +13 -0
- package/dist/polyfill-connectors/src/static-secret-injection.js +155 -0
- package/package.json +1 -1
|
@@ -4,10 +4,12 @@ import { mkdirSync, writeFileSync } from "node:fs";
|
|
|
4
4
|
import { delimiter, join } from "node:path";
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { buildAgentVersion } from "./collector-build-info.js";
|
|
7
8
|
import { LocalDeviceClient, } from "./local-device-client.js";
|
|
8
9
|
import { buildLocalDeviceRecordEnvelope, hashCanonicalJson, } from "./local-device-envelope.js";
|
|
9
10
|
import { buildLocalDeviceOutboxId, LocalDeviceOutbox, } from "./local-device-outbox.js";
|
|
10
11
|
import { assertPlacementOrThrow, COLLECTOR_RUNTIME_CAPABILITIES, } from "./runtime-capabilities.js";
|
|
12
|
+
const COLLECTOR_AGENT_VERSION = buildAgentVersion();
|
|
11
13
|
export const COLLECTOR_STDERR_MAX_BYTES = 256 * 1024;
|
|
12
14
|
const COLLECTOR_GAP_DETAILS_MAX_CHARS = 300;
|
|
13
15
|
const KEYED_SECRET_RE = /\b(authorization|bearer|token|password|passwd|cookie|secret|otp|api[_-]?key)\b\s*[:=]\s*["']?[^"',\s}]+/gi;
|
|
@@ -17,7 +19,7 @@ const SCAN_BATCH_LIMIT_DETAIL_RE = /enqueued\s+\d+\s+batches\s+>=\s+(?:run batch
|
|
|
17
19
|
const CONNECTOR_PROTOCOL_DEBUG_DIR_ENV = "PDPP_DEBUG_CONNECTOR_PROTOCOL_DIR";
|
|
18
20
|
export const DEFAULT_COLLECTOR_OUTBOX_POLICY = Object.freeze({
|
|
19
21
|
drainBatchSize: 4,
|
|
20
|
-
leaseMs:
|
|
22
|
+
leaseMs: 300_000,
|
|
21
23
|
maxAttempts: 5,
|
|
22
24
|
maxDrainDurationMs: 120_000,
|
|
23
25
|
maxDrainIterations: 256,
|
|
@@ -25,6 +27,71 @@ export const DEFAULT_COLLECTOR_OUTBOX_POLICY = Object.freeze({
|
|
|
25
27
|
maxQueueDepth: 10_000,
|
|
26
28
|
retryBackoffMs: 30_000,
|
|
27
29
|
});
|
|
30
|
+
export const DEFAULT_COLLECTOR_AUTO_PRUNE_POLICY = Object.freeze({
|
|
31
|
+
enabled: true,
|
|
32
|
+
keepRecentCount: 10_000,
|
|
33
|
+
});
|
|
34
|
+
export function resolveCollectorAutoPrunePolicy(override, env = process.env) {
|
|
35
|
+
const policy = { ...DEFAULT_COLLECTOR_AUTO_PRUNE_POLICY, ...(override ?? {}) };
|
|
36
|
+
const enabledRaw = env.PDPP_COLLECTOR_AUTO_PRUNE;
|
|
37
|
+
if (typeof enabledRaw === "string" && enabledRaw.trim() !== "") {
|
|
38
|
+
policy.enabled = !DISABLED_ENV_VALUES.has(enabledRaw.trim().toLowerCase());
|
|
39
|
+
}
|
|
40
|
+
const keepCount = parseNonNegativeInt(env.PDPP_COLLECTOR_AUTO_PRUNE_KEEP_COUNT);
|
|
41
|
+
if (keepCount !== null) {
|
|
42
|
+
policy.keepRecentCount = keepCount;
|
|
43
|
+
}
|
|
44
|
+
return policy;
|
|
45
|
+
}
|
|
46
|
+
const DISABLED_ENV_VALUES = new Set(["0", "false", "off", "no"]);
|
|
47
|
+
function parseNonNegativeInt(raw) {
|
|
48
|
+
if (typeof raw !== "string" || raw.trim() === "") {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const value = Number(raw.trim());
|
|
52
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
53
|
+
}
|
|
54
|
+
export function autoPruneSucceededOutbox(input) {
|
|
55
|
+
if (!input.policy.enabled) {
|
|
56
|
+
return { enabled: false, matched: 0, pruned: 0 };
|
|
57
|
+
}
|
|
58
|
+
const result = input.outbox.pruneSent({
|
|
59
|
+
dryRun: false,
|
|
60
|
+
keepCount: input.policy.keepRecentCount,
|
|
61
|
+
sourceInstanceId: input.sourceInstanceId,
|
|
62
|
+
});
|
|
63
|
+
return { enabled: true, matched: result.matched, pruned: result.pruned };
|
|
64
|
+
}
|
|
65
|
+
export const DEFAULT_COLLECTOR_AUTO_COMPACT_POLICY = Object.freeze({
|
|
66
|
+
enabled: true,
|
|
67
|
+
minReclaimableBytes: 512 * 1024 * 1024,
|
|
68
|
+
});
|
|
69
|
+
export function resolveCollectorAutoCompactPolicy(override, env = process.env) {
|
|
70
|
+
const policy = { ...DEFAULT_COLLECTOR_AUTO_COMPACT_POLICY, ...(override ?? {}) };
|
|
71
|
+
const enabledRaw = env.PDPP_COLLECTOR_AUTO_COMPACT;
|
|
72
|
+
if (typeof enabledRaw === "string" && enabledRaw.trim() !== "") {
|
|
73
|
+
policy.enabled = !DISABLED_ENV_VALUES.has(enabledRaw.trim().toLowerCase());
|
|
74
|
+
}
|
|
75
|
+
const minBytes = parseNonNegativeInt(env.PDPP_COLLECTOR_AUTO_COMPACT_MIN_RECLAIM_BYTES);
|
|
76
|
+
if (minBytes !== null) {
|
|
77
|
+
policy.minReclaimableBytes = minBytes;
|
|
78
|
+
}
|
|
79
|
+
return policy;
|
|
80
|
+
}
|
|
81
|
+
export function autoCompactOutboxIfBloated(input) {
|
|
82
|
+
if (!input.policy.enabled) {
|
|
83
|
+
return { compacted: false, enabled: false, reason: "disabled", reclaimedBytes: 0 };
|
|
84
|
+
}
|
|
85
|
+
const before = input.outbox.pageStats();
|
|
86
|
+
if (before.reclaimableBytes < input.policy.minReclaimableBytes) {
|
|
87
|
+
return { compacted: false, enabled: true, reason: "below_threshold", reclaimedBytes: 0 };
|
|
88
|
+
}
|
|
89
|
+
if (input.outbox.countNonSucceeded() > 0) {
|
|
90
|
+
return { compacted: false, enabled: true, reason: "lane_not_quiet", reclaimedBytes: 0 };
|
|
91
|
+
}
|
|
92
|
+
const result = input.outbox.compact();
|
|
93
|
+
return { compacted: true, enabled: true, reason: "compacted", reclaimedBytes: result.reclaimedBytes };
|
|
94
|
+
}
|
|
28
95
|
const PACKAGE_ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
29
96
|
const REPO_ROOT = join(PACKAGE_ROOT, "..", "..");
|
|
30
97
|
export async function enrollCollector(config) {
|
|
@@ -34,6 +101,15 @@ export async function enrollCollector(config) {
|
|
|
34
101
|
...(config.deviceLabel ? { deviceLabel: config.deviceLabel } : {}),
|
|
35
102
|
});
|
|
36
103
|
}
|
|
104
|
+
export const COLLECTOR_COVERAGE_STATUSES = [
|
|
105
|
+
"collected",
|
|
106
|
+
"inventory_only",
|
|
107
|
+
"excluded",
|
|
108
|
+
"deferred",
|
|
109
|
+
"missing",
|
|
110
|
+
"unsupported",
|
|
111
|
+
"unaccounted",
|
|
112
|
+
];
|
|
37
113
|
export class CollectorStateReadError extends Error {
|
|
38
114
|
constructor(message, cause) {
|
|
39
115
|
super(message, { cause });
|
|
@@ -44,6 +120,8 @@ export async function runCollectorConnector(config) {
|
|
|
44
120
|
throwIfAborted(config.abortSignal);
|
|
45
121
|
const satisfiedBindings = assertPlacementOrThrow(config.connector, COLLECTOR_RUNTIME_CAPABILITIES);
|
|
46
122
|
const policy = { ...DEFAULT_COLLECTOR_OUTBOX_POLICY, ...(config.outboxPolicy ?? {}) };
|
|
123
|
+
const autoPrunePolicy = resolveCollectorAutoPrunePolicy(config.autoPrune);
|
|
124
|
+
const autoCompactPolicy = resolveCollectorAutoCompactPolicy(config.autoCompact);
|
|
47
125
|
const holderId = config.collectorHolderId ?? randomUUID();
|
|
48
126
|
const outboxPath = config.outboxPath ?? config.queuePath;
|
|
49
127
|
const outbox = new LocalDeviceOutbox({ path: outboxPath });
|
|
@@ -52,6 +130,7 @@ export async function runCollectorConnector(config) {
|
|
|
52
130
|
deviceId: config.deviceId,
|
|
53
131
|
deviceToken: config.deviceToken,
|
|
54
132
|
});
|
|
133
|
+
let startingHeartbeatSent = false;
|
|
55
134
|
try {
|
|
56
135
|
const recoveredLeases = outbox.recoverExpiredLeases({ sourceInstanceId: config.sourceInstanceId });
|
|
57
136
|
const preScanDrain = await drainCollectorOutbox({
|
|
@@ -65,6 +144,7 @@ export async function runCollectorConnector(config) {
|
|
|
65
144
|
});
|
|
66
145
|
const postDrainSummary = outbox.summary({ sourceInstanceId: config.sourceInstanceId });
|
|
67
146
|
const skipResult = await maybeSkipScanForBacklog({
|
|
147
|
+
autoPrunePolicy,
|
|
68
148
|
client,
|
|
69
149
|
config,
|
|
70
150
|
outbox,
|
|
@@ -83,6 +163,7 @@ export async function runCollectorConnector(config) {
|
|
|
83
163
|
recordsPending: pendingOutboxWorkCount(postDrainSummary),
|
|
84
164
|
});
|
|
85
165
|
await client.heartbeat({
|
|
166
|
+
agent_version: COLLECTOR_AGENT_VERSION,
|
|
86
167
|
connector_id: config.connector.connector_id,
|
|
87
168
|
outbox: buildHeartbeatOutboxDiagnostics(postDrainSummary, {
|
|
88
169
|
backlogOpen: countOpenBacklogGaps(outbox, config.sourceInstanceId),
|
|
@@ -91,6 +172,7 @@ export async function runCollectorConnector(config) {
|
|
|
91
172
|
source_instance_id: config.sourceInstanceId,
|
|
92
173
|
status: "starting",
|
|
93
174
|
});
|
|
175
|
+
startingHeartbeatSent = true;
|
|
94
176
|
const streamResult = await streamConnectorIntoOutbox({
|
|
95
177
|
...(config.abortSignal ? { abortSignal: config.abortSignal } : {}),
|
|
96
178
|
batchSize: config.batchSize ?? 100,
|
|
@@ -130,11 +212,20 @@ export async function runCollectorConnector(config) {
|
|
|
130
212
|
deferRecoveredGapCleanup: streamResult.scanBudgetExceeded,
|
|
131
213
|
outbox,
|
|
132
214
|
});
|
|
215
|
+
const prunedSent = autoPruneSucceededOutbox({
|
|
216
|
+
outbox,
|
|
217
|
+
policy: autoPrunePolicy,
|
|
218
|
+
sourceInstanceId: config.sourceInstanceId,
|
|
219
|
+
});
|
|
220
|
+
autoCompactOutboxIfBloated({ outbox, policy: autoCompactPolicy });
|
|
133
221
|
const finalSummary = outbox.summary({ sourceInstanceId: config.sourceInstanceId });
|
|
134
222
|
const recordsPending = pendingOutboxWorkCount(finalSummary);
|
|
135
223
|
if (!checkpointResult.statePutFailed) {
|
|
224
|
+
const finalDeadLetterError = buildHeartbeatDeadLetterError(outbox, config.sourceInstanceId);
|
|
136
225
|
await client.heartbeat({
|
|
226
|
+
agent_version: COLLECTOR_AGENT_VERSION,
|
|
137
227
|
connector_id: config.connector.connector_id,
|
|
228
|
+
...(finalDeadLetterError ? { last_error: finalDeadLetterError } : {}),
|
|
138
229
|
outbox: buildHeartbeatOutboxDiagnostics(finalSummary, {
|
|
139
230
|
backlogOpen: countOpenBacklogGaps(outbox, config.sourceInstanceId),
|
|
140
231
|
}),
|
|
@@ -144,11 +235,13 @@ export async function runCollectorConnector(config) {
|
|
|
144
235
|
});
|
|
145
236
|
}
|
|
146
237
|
return {
|
|
238
|
+
completeness: summarizeCollectorCompleteness(streamResult.coverageByStore),
|
|
147
239
|
done,
|
|
148
240
|
enqueuedBatches: enqueueResult.enqueuedBatches,
|
|
149
241
|
flushedState: checkpointResult.flushedState,
|
|
150
242
|
outboxSummary: finalSummary,
|
|
151
243
|
priorState,
|
|
244
|
+
prunedSent,
|
|
152
245
|
recordsQueued: enqueueResult.recordsQueued,
|
|
153
246
|
recoveredLeases,
|
|
154
247
|
satisfiedBindings,
|
|
@@ -159,10 +252,30 @@ export async function runCollectorConnector(config) {
|
|
|
159
252
|
streamingBufferHighWaterMark: streamResult.bufferHighWaterMark,
|
|
160
253
|
};
|
|
161
254
|
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
if (startingHeartbeatSent) {
|
|
257
|
+
await emitCorrectiveHeartbeatFromOutbox({ client, config, outbox, policy });
|
|
258
|
+
}
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
162
261
|
finally {
|
|
163
262
|
outbox.close();
|
|
164
263
|
}
|
|
165
264
|
}
|
|
265
|
+
async function emitCorrectiveHeartbeatFromOutbox(input) {
|
|
266
|
+
const summary = input.outbox.summary({ sourceInstanceId: input.config.sourceInstanceId });
|
|
267
|
+
const deadLetterError = buildHeartbeatDeadLetterError(input.outbox, input.config.sourceInstanceId);
|
|
268
|
+
await safeHeartbeat(input.client, {
|
|
269
|
+
connector_id: input.config.connector.connector_id,
|
|
270
|
+
...(deadLetterError ? { last_error: deadLetterError } : {}),
|
|
271
|
+
outbox: buildHeartbeatOutboxDiagnostics(summary, {
|
|
272
|
+
backlogOpen: countOpenBacklogGaps(input.outbox, input.config.sourceInstanceId),
|
|
273
|
+
}),
|
|
274
|
+
records_pending: pendingOutboxWorkCount(summary),
|
|
275
|
+
source_instance_id: input.config.sourceInstanceId,
|
|
276
|
+
status: heartbeatStatusForSummary(summary, input.policy),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
166
279
|
async function maybeSkipScanForBacklog(input) {
|
|
167
280
|
if (!hasScanBlockingOutboxWork(input.outbox, input.config.sourceInstanceId, input.policy)) {
|
|
168
281
|
return null;
|
|
@@ -180,6 +293,11 @@ async function maybeSkipScanForBacklog(input) {
|
|
|
180
293
|
sourceInstanceId: input.config.sourceInstanceId,
|
|
181
294
|
});
|
|
182
295
|
}
|
|
296
|
+
const prunedSent = autoPruneSucceededOutbox({
|
|
297
|
+
outbox: input.outbox,
|
|
298
|
+
policy: input.autoPrunePolicy,
|
|
299
|
+
sourceInstanceId: input.config.sourceInstanceId,
|
|
300
|
+
});
|
|
183
301
|
const summaryAfterGap = input.outbox.summary({ sourceInstanceId: input.config.sourceInstanceId });
|
|
184
302
|
const recordsPendingAfterGap = pendingOutboxWorkCount(summaryAfterGap);
|
|
185
303
|
await safeHeartbeat(input.client, {
|
|
@@ -192,11 +310,13 @@ async function maybeSkipScanForBacklog(input) {
|
|
|
192
310
|
status: heartbeatStatusForSummary(summaryAfterGap, input.policy),
|
|
193
311
|
});
|
|
194
312
|
return {
|
|
313
|
+
completeness: null,
|
|
195
314
|
done: null,
|
|
196
315
|
enqueuedBatches: 0,
|
|
197
316
|
flushedState: null,
|
|
198
317
|
outboxSummary: summaryAfterGap,
|
|
199
318
|
priorState: Object.freeze({}),
|
|
319
|
+
prunedSent,
|
|
200
320
|
recordsQueued: 0,
|
|
201
321
|
recoveredLeases: input.recoveredLeases,
|
|
202
322
|
satisfiedBindings: input.satisfiedBindings,
|
|
@@ -218,6 +338,7 @@ async function readPriorStateOrBlock(input) {
|
|
|
218
338
|
catch (error) {
|
|
219
339
|
await safeHeartbeat(input.client, {
|
|
220
340
|
connector_id: input.config.connector.connector_id,
|
|
341
|
+
last_error: { kind: "state_read_failed" },
|
|
221
342
|
records_pending: input.recordsPending,
|
|
222
343
|
source_instance_id: input.config.sourceInstanceId,
|
|
223
344
|
status: "blocked",
|
|
@@ -225,6 +346,50 @@ async function readPriorStateOrBlock(input) {
|
|
|
225
346
|
throw new CollectorStateReadError(`failed to read prior state for ${input.config.sourceInstanceId}: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
226
347
|
}
|
|
227
348
|
}
|
|
349
|
+
const COVERAGE_DIAGNOSTICS_STREAM = "coverage_diagnostics";
|
|
350
|
+
function coverageEntryFromRecord(message) {
|
|
351
|
+
if (message.stream !== COVERAGE_DIAGNOSTICS_STREAM) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
const data = message.data;
|
|
355
|
+
const dataStore = isRecord(data) && typeof data.store === "string" && data.store ? data.store : null;
|
|
356
|
+
const keyStore = typeof message.key === "string" && message.key ? message.key : null;
|
|
357
|
+
const store = dataStore ?? keyStore;
|
|
358
|
+
if (!store) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
const rawStatus = isRecord(data) && typeof data.status === "string" ? data.status : null;
|
|
362
|
+
if (!rawStatus) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
const status = COLLECTOR_COVERAGE_STATUSES.includes(rawStatus)
|
|
366
|
+
? rawStatus
|
|
367
|
+
: "unaccounted";
|
|
368
|
+
return { status, store };
|
|
369
|
+
}
|
|
370
|
+
export function summarizeCollectorCompleteness(coverageByStore) {
|
|
371
|
+
if (!coverageByStore || coverageByStore.size === 0) {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
const countsByStatus = Object.fromEntries(COLLECTOR_COVERAGE_STATUSES.map((status) => [status, 0]));
|
|
375
|
+
const unaccountedStores = [];
|
|
376
|
+
const byStore = {};
|
|
377
|
+
for (const store of [...coverageByStore.keys()].sort()) {
|
|
378
|
+
const status = coverageByStore.get(store);
|
|
379
|
+
byStore[store] = status;
|
|
380
|
+
countsByStatus[status] += 1;
|
|
381
|
+
if (status === "unaccounted") {
|
|
382
|
+
unaccountedStores.push(store);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
byStore,
|
|
387
|
+
countsByStatus,
|
|
388
|
+
fullyAccounted: unaccountedStores.length === 0,
|
|
389
|
+
storeCount: coverageByStore.size,
|
|
390
|
+
unaccountedStores,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
228
393
|
async function streamConnectorIntoOutbox(input) {
|
|
229
394
|
throwIfAborted(input.abortSignal);
|
|
230
395
|
const child = spawnConnector(input.config.connector, {
|
|
@@ -242,6 +407,7 @@ async function streamConnectorIntoOutbox(input) {
|
|
|
242
407
|
let enqueuedBatches = 0;
|
|
243
408
|
let done = null;
|
|
244
409
|
let scanBudgetExceeded = false;
|
|
410
|
+
let coverageByStore = null;
|
|
245
411
|
const flushPendingBatch = () => {
|
|
246
412
|
if (pendingRecords.length === 0) {
|
|
247
413
|
return;
|
|
@@ -288,8 +454,17 @@ async function streamConnectorIntoOutbox(input) {
|
|
|
288
454
|
scanBudgetExceeded,
|
|
289
455
|
});
|
|
290
456
|
};
|
|
457
|
+
const recordCoverageIfPresent = (message) => {
|
|
458
|
+
const entry = coverageEntryFromRecord(message);
|
|
459
|
+
if (!entry) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
coverageByStore ??= new Map();
|
|
463
|
+
coverageByStore.set(entry.store, entry.status);
|
|
464
|
+
};
|
|
291
465
|
const handleMessage = (message) => {
|
|
292
466
|
if (message.type === "RECORD") {
|
|
467
|
+
recordCoverageIfPresent(message);
|
|
293
468
|
pendingRecords.push(message);
|
|
294
469
|
if (pendingRecords.length > bufferHighWaterMark) {
|
|
295
470
|
bufferHighWaterMark = pendingRecords.length;
|
|
@@ -397,6 +572,7 @@ async function streamConnectorIntoOutbox(input) {
|
|
|
397
572
|
return {
|
|
398
573
|
bufferedState: Object.freeze(scanBudgetExceeded ? {} : { ...bufferedState }),
|
|
399
574
|
bufferHighWaterMark,
|
|
575
|
+
coverageByStore,
|
|
400
576
|
done: scanBudgetExceeded ? null : done,
|
|
401
577
|
enqueuedBatches,
|
|
402
578
|
recordsQueued,
|
|
@@ -567,7 +743,7 @@ async function recoverResolvedLocalCollectorGaps(input) {
|
|
|
567
743
|
}
|
|
568
744
|
async function safeHeartbeat(client, request) {
|
|
569
745
|
try {
|
|
570
|
-
await client.heartbeat(request);
|
|
746
|
+
await client.heartbeat({ agent_version: COLLECTOR_AGENT_VERSION, ...request });
|
|
571
747
|
}
|
|
572
748
|
catch {
|
|
573
749
|
}
|
|
@@ -715,35 +891,53 @@ function hasCheckpointPredecessorBlockingWork(outbox, checkpoint) {
|
|
|
715
891
|
async function drainClaimedOutboxItem(input, item, result, sentByKind) {
|
|
716
892
|
throwIfAborted(input.abortSignal);
|
|
717
893
|
try {
|
|
718
|
-
|
|
719
|
-
|
|
894
|
+
const current = input.outbox.renewLease({
|
|
895
|
+
holder: input.holderId,
|
|
896
|
+
id: item.id,
|
|
897
|
+
leaseEpoch: item.lease_epoch,
|
|
898
|
+
leaseMs: input.policy.leaseMs,
|
|
899
|
+
});
|
|
900
|
+
await sendOutboxItem(input.client, current);
|
|
901
|
+
input.outbox.acknowledge({ holder: input.holderId, id: current.id, leaseEpoch: current.lease_epoch });
|
|
720
902
|
result.sent++;
|
|
721
|
-
sentByKind[
|
|
903
|
+
sentByKind[current.kind] = (sentByKind[current.kind] ?? 0) + 1;
|
|
722
904
|
}
|
|
723
905
|
catch (error) {
|
|
724
906
|
failOutboxItem(input, item, error, result);
|
|
725
907
|
}
|
|
726
908
|
}
|
|
727
909
|
function failOutboxItem(input, item, error, result) {
|
|
728
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
729
|
-
|
|
730
|
-
input.
|
|
910
|
+
const message = sanitizeCollectorGapDetails(error instanceof Error ? error.message : String(error));
|
|
911
|
+
try {
|
|
912
|
+
if (error instanceof OutboxPayloadShapeError || item.attempt_count + 1 >= input.policy.maxAttempts) {
|
|
913
|
+
input.outbox.deadLetter({
|
|
914
|
+
error: message,
|
|
915
|
+
holder: input.holderId,
|
|
916
|
+
id: item.id,
|
|
917
|
+
leaseEpoch: item.lease_epoch,
|
|
918
|
+
});
|
|
919
|
+
result.deadLettered++;
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
input.outbox.failRetryable({
|
|
731
923
|
error: message,
|
|
732
924
|
holder: input.holderId,
|
|
733
925
|
id: item.id,
|
|
734
926
|
leaseEpoch: item.lease_epoch,
|
|
927
|
+
retryBackoffMs: input.policy.retryBackoffMs * (item.attempt_count + 1),
|
|
735
928
|
});
|
|
736
|
-
result.
|
|
737
|
-
return;
|
|
929
|
+
result.failed++;
|
|
738
930
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
}
|
|
746
|
-
|
|
931
|
+
catch (transitionError) {
|
|
932
|
+
if (isLeaseNotCurrentError(transitionError)) {
|
|
933
|
+
result.failed++;
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
throw transitionError;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
function isLeaseNotCurrentError(error) {
|
|
940
|
+
return error instanceof Error && error.message.startsWith("local outbox lease not current");
|
|
747
941
|
}
|
|
748
942
|
class OutboxPayloadShapeError extends Error {
|
|
749
943
|
constructor(message) {
|
|
@@ -937,6 +1131,34 @@ function isUnresolvedScanBudgetGap(item, policy) {
|
|
|
937
1131
|
}
|
|
938
1132
|
return policy.maxEnqueuedBatchesPerRun <= Number(match[1]);
|
|
939
1133
|
}
|
|
1134
|
+
export const LOCAL_COLLECTOR_LIFECYCLE_STATES = [
|
|
1135
|
+
"healthy_idle",
|
|
1136
|
+
"draining",
|
|
1137
|
+
"retryable_backlog",
|
|
1138
|
+
"dead_letter",
|
|
1139
|
+
"stale_lease",
|
|
1140
|
+
"coverage_missing",
|
|
1141
|
+
];
|
|
1142
|
+
export function deriveLocalCollectorLifecycleState(input) {
|
|
1143
|
+
const { summary } = input;
|
|
1144
|
+
if (summary.deadLetter > 0) {
|
|
1145
|
+
return "dead_letter";
|
|
1146
|
+
}
|
|
1147
|
+
if (summary.staleLeases > 0) {
|
|
1148
|
+
return "stale_lease";
|
|
1149
|
+
}
|
|
1150
|
+
const claimableNow = Math.max(0, summary.ready - summary.retrying);
|
|
1151
|
+
if (summary.leased > 0 || claimableNow > 0) {
|
|
1152
|
+
return "draining";
|
|
1153
|
+
}
|
|
1154
|
+
if (summary.retrying > 0) {
|
|
1155
|
+
return "retryable_backlog";
|
|
1156
|
+
}
|
|
1157
|
+
if (input.coverageObserved === false && input.recordBatchCount > 0) {
|
|
1158
|
+
return "coverage_missing";
|
|
1159
|
+
}
|
|
1160
|
+
return "healthy_idle";
|
|
1161
|
+
}
|
|
940
1162
|
function heartbeatStatusForSummary(summary, policy) {
|
|
941
1163
|
if (summary.deadLetter > 0) {
|
|
942
1164
|
return "blocked";
|
|
@@ -963,6 +1185,16 @@ export function buildHeartbeatOutboxDiagnostics(summary, options = {}) {
|
|
|
963
1185
|
total: summary.total,
|
|
964
1186
|
};
|
|
965
1187
|
}
|
|
1188
|
+
function buildHeartbeatDeadLetterError(outbox, sourceInstanceId) {
|
|
1189
|
+
const summary = outbox.deadLetterErrorSummary({ sourceInstanceId });
|
|
1190
|
+
if (summary.dead_letter_count === 0) {
|
|
1191
|
+
return null;
|
|
1192
|
+
}
|
|
1193
|
+
return {
|
|
1194
|
+
kind: "dead_letter_backlog",
|
|
1195
|
+
top_dead_letter_classes: summary.top_classes,
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
966
1198
|
function countOpenBacklogGaps(outbox, sourceInstanceId) {
|
|
967
1199
|
return outbox.countOpenGaps({ sourceInstanceId });
|
|
968
1200
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const STDOUT_DRAIN_TIMEOUT_MS = 3000;
|
|
2
|
+
const RUNTIME_ACK_TIMEOUT_MS = 30 * 60 * 1000;
|
|
3
|
+
export function flushAndExitAfterRuntimeAck(code, options = {}) {
|
|
4
|
+
const stdin = options.stdin ?? process.stdin;
|
|
5
|
+
const stdout = options.stdout ?? process.stdout;
|
|
6
|
+
const exit = options.exit ?? process.exit;
|
|
7
|
+
const stdoutDrainTimeoutMs = options.stdoutDrainTimeoutMs ?? STDOUT_DRAIN_TIMEOUT_MS;
|
|
8
|
+
const runtimeAckTimeoutMs = options.runtimeAckTimeoutMs ?? RUNTIME_ACK_TIMEOUT_MS;
|
|
9
|
+
let finished = false;
|
|
10
|
+
let drainTimer = null;
|
|
11
|
+
let ackTimer = null;
|
|
12
|
+
const finish = () => {
|
|
13
|
+
if (finished) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
finished = true;
|
|
17
|
+
if (drainTimer) {
|
|
18
|
+
clearTimeout(drainTimer);
|
|
19
|
+
}
|
|
20
|
+
if (ackTimer) {
|
|
21
|
+
clearTimeout(ackTimer);
|
|
22
|
+
}
|
|
23
|
+
exit(code);
|
|
24
|
+
};
|
|
25
|
+
const waitForRuntimeAck = () => {
|
|
26
|
+
if (drainTimer) {
|
|
27
|
+
clearTimeout(drainTimer);
|
|
28
|
+
drainTimer = null;
|
|
29
|
+
}
|
|
30
|
+
if (stdin.readableEnded || stdin.destroyed) {
|
|
31
|
+
finish();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const cleanup = () => {
|
|
35
|
+
stdin.off("close", onRuntimeAck);
|
|
36
|
+
stdin.off("end", onRuntimeAck);
|
|
37
|
+
stdin.off("error", onRuntimeAck);
|
|
38
|
+
};
|
|
39
|
+
const onRuntimeAck = () => {
|
|
40
|
+
cleanup();
|
|
41
|
+
finish();
|
|
42
|
+
};
|
|
43
|
+
stdin.once("close", onRuntimeAck);
|
|
44
|
+
stdin.once("end", onRuntimeAck);
|
|
45
|
+
stdin.once("error", onRuntimeAck);
|
|
46
|
+
ackTimer = setTimeout(() => {
|
|
47
|
+
cleanup();
|
|
48
|
+
finish();
|
|
49
|
+
}, runtimeAckTimeoutMs);
|
|
50
|
+
ackTimer.unref();
|
|
51
|
+
};
|
|
52
|
+
if (stdout.writableLength > 0) {
|
|
53
|
+
stdout.once("drain", waitForRuntimeAck);
|
|
54
|
+
drainTimer = setTimeout(() => {
|
|
55
|
+
stdout.off("drain", waitForRuntimeAck);
|
|
56
|
+
waitForRuntimeAck();
|
|
57
|
+
}, stdoutDrainTimeoutMs);
|
|
58
|
+
drainTimer.unref();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
waitForRuntimeAck();
|
|
62
|
+
}
|
|
@@ -31,6 +31,19 @@ export interface DetailGapStartEntry {
|
|
|
31
31
|
status: "pending";
|
|
32
32
|
stream: string;
|
|
33
33
|
}
|
|
34
|
+
export interface DetailGapsPageRequestMessage {
|
|
35
|
+
max_bytes?: number;
|
|
36
|
+
reference_only: true;
|
|
37
|
+
request_id: string;
|
|
38
|
+
streams?: readonly string[];
|
|
39
|
+
type: "DETAIL_GAPS_PAGE_REQUEST";
|
|
40
|
+
}
|
|
41
|
+
export interface DetailGapsPageResponse {
|
|
42
|
+
detail_gaps: readonly DetailGapStartEntry[];
|
|
43
|
+
reference_only: true;
|
|
44
|
+
request_id: string;
|
|
45
|
+
type: "DETAIL_GAPS_PAGE_RESPONSE";
|
|
46
|
+
}
|
|
34
47
|
export interface InteractionResponse {
|
|
35
48
|
data?: Record<string, string>;
|
|
36
49
|
error?: {
|
|
@@ -70,20 +83,21 @@ export interface AssistanceCompletion {
|
|
|
70
83
|
message?: string;
|
|
71
84
|
status: AssistanceCompletionStatus;
|
|
72
85
|
}
|
|
86
|
+
export interface DetailGapNetworkPressure {
|
|
87
|
+
attempt?: number;
|
|
88
|
+
endpoint_route: string;
|
|
89
|
+
error_class: string;
|
|
90
|
+
max_attempts?: number;
|
|
91
|
+
method: string;
|
|
92
|
+
retry_after_ms?: number;
|
|
93
|
+
safe_headers?: Record<string, string | number>;
|
|
94
|
+
status?: number;
|
|
95
|
+
}
|
|
73
96
|
export interface DetailGapMessage {
|
|
74
97
|
detail?: {
|
|
75
98
|
class?: string;
|
|
76
99
|
http_status?: number;
|
|
77
|
-
network_pressure?:
|
|
78
|
-
attempt?: number;
|
|
79
|
-
endpoint_route: string;
|
|
80
|
-
error_class: string;
|
|
81
|
-
max_attempts?: number;
|
|
82
|
-
method: string;
|
|
83
|
-
retry_after_ms?: number;
|
|
84
|
-
safe_headers?: Record<string, string | number>;
|
|
85
|
-
status?: number;
|
|
86
|
-
};
|
|
100
|
+
network_pressure?: DetailGapNetworkPressure;
|
|
87
101
|
};
|
|
88
102
|
detail_locator: {
|
|
89
103
|
kind: string;
|
|
@@ -93,16 +107,7 @@ export interface DetailGapMessage {
|
|
|
93
107
|
class?: string;
|
|
94
108
|
http_status?: number;
|
|
95
109
|
message?: string;
|
|
96
|
-
network_pressure?:
|
|
97
|
-
attempt?: number;
|
|
98
|
-
endpoint_route: string;
|
|
99
|
-
error_class: string;
|
|
100
|
-
max_attempts?: number;
|
|
101
|
-
method: string;
|
|
102
|
-
retry_after_ms?: number;
|
|
103
|
-
safe_headers?: Record<string, string | number>;
|
|
104
|
-
status?: number;
|
|
105
|
-
};
|
|
110
|
+
network_pressure?: DetailGapNetworkPressure;
|
|
106
111
|
};
|
|
107
112
|
list_cursor?: unknown;
|
|
108
113
|
parent_stream?: string;
|
|
@@ -115,6 +120,8 @@ export interface DetailGapMessage {
|
|
|
115
120
|
type: "DETAIL_GAP";
|
|
116
121
|
}
|
|
117
122
|
export interface DetailCoverageMessage {
|
|
123
|
+
considered?: number;
|
|
124
|
+
covered?: number;
|
|
118
125
|
gap_keys?: Array<string | number>;
|
|
119
126
|
hydrated_keys: Array<string | number>;
|
|
120
127
|
optional_skip_keys?: Array<string | number>;
|
|
@@ -131,6 +138,18 @@ export interface DetailGapRecoveredMessage {
|
|
|
131
138
|
stream: string;
|
|
132
139
|
type: "DETAIL_GAP_RECOVERED";
|
|
133
140
|
}
|
|
141
|
+
export interface ProviderBudgetProgress {
|
|
142
|
+
circuit: {
|
|
143
|
+
previous_state: "closed" | "half_open" | "open";
|
|
144
|
+
reason: "provider_failure" | "provider_throttle" | "reset_timeout" | "success";
|
|
145
|
+
state: "closed" | "half_open" | "open";
|
|
146
|
+
trigger: "before_request" | "provider_failure" | "provider_throttle" | "success";
|
|
147
|
+
};
|
|
148
|
+
elapsed_ms: number;
|
|
149
|
+
object: "provider_budget_circuit_transition";
|
|
150
|
+
request_count: number;
|
|
151
|
+
retry_tokens_remaining?: number | "unbounded";
|
|
152
|
+
}
|
|
134
153
|
export type EmittedMessage = {
|
|
135
154
|
type: "RECORD";
|
|
136
155
|
stream: string;
|
|
@@ -146,6 +165,7 @@ export type EmittedMessage = {
|
|
|
146
165
|
type: "PROGRESS";
|
|
147
166
|
message: string;
|
|
148
167
|
stream?: string;
|
|
168
|
+
provider_budget?: ProviderBudgetProgress;
|
|
149
169
|
} | ({
|
|
150
170
|
type: "ASSISTANCE";
|
|
151
171
|
} & AssistanceRequest) | ({
|
|
@@ -156,7 +176,7 @@ export type EmittedMessage = {
|
|
|
156
176
|
reason: string;
|
|
157
177
|
message: string;
|
|
158
178
|
diagnostics?: unknown;
|
|
159
|
-
} | DetailGapMessage | DetailCoverageMessage | DetailGapRecoveredMessage | {
|
|
179
|
+
} | DetailGapMessage | DetailCoverageMessage | DetailGapRecoveredMessage | DetailGapsPageRequestMessage | {
|
|
160
180
|
type: "DONE";
|
|
161
181
|
status: "succeeded" | "failed";
|
|
162
182
|
records_emitted: number;
|