@pdpp/local-collector 0.1.0-beta.6 → 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.
Files changed (26) hide show
  1. package/dist/local-collector/bin/pdpp-local-collector.js +580 -22
  2. package/dist/local-collector/src/runner.d.ts +1 -1
  3. package/dist/local-collector/src/runner.js +15 -1
  4. package/dist/polyfill-connectors/connectors/claude_code/index.js +85 -48
  5. package/dist/polyfill-connectors/connectors/codex/index.js +390 -108
  6. package/dist/polyfill-connectors/connectors/codex/parsers.js +5 -3
  7. package/dist/polyfill-connectors/src/bounded-file-preview.js +76 -0
  8. package/dist/polyfill-connectors/src/browser-handoff.js +38 -5
  9. package/dist/polyfill-connectors/src/collector-build-info.d.ts +8 -0
  10. package/dist/polyfill-connectors/src/collector-build-info.js +10 -0
  11. package/dist/polyfill-connectors/src/collector-runner.d.ts +54 -0
  12. package/dist/polyfill-connectors/src/collector-runner.js +250 -18
  13. package/dist/polyfill-connectors/src/connector-exit.js +62 -0
  14. package/dist/polyfill-connectors/src/connector-runtime-protocol.d.ts +41 -21
  15. package/dist/polyfill-connectors/src/connector-runtime.js +241 -30
  16. package/dist/polyfill-connectors/src/fingerprint-cursor.js +107 -0
  17. package/dist/polyfill-connectors/src/local-device-client.d.ts +17 -0
  18. package/dist/polyfill-connectors/src/local-device-client.js +69 -9
  19. package/dist/polyfill-connectors/src/local-device-outbox.d.ts +59 -0
  20. package/dist/polyfill-connectors/src/local-device-outbox.js +394 -5
  21. package/dist/polyfill-connectors/src/local-source-inventory.js +8 -1
  22. package/dist/polyfill-connectors/src/runner/index.d.ts +4 -3
  23. package/dist/polyfill-connectors/src/runner/index.js +4 -3
  24. package/dist/polyfill-connectors/src/safe-text-preview.js +13 -0
  25. package/dist/polyfill-connectors/src/static-secret-injection.js +155 -0
  26. package/package.json +1 -1
@@ -66,6 +66,50 @@ export interface LocalDeviceOutboxDeadLetterInput extends LocalDeviceOutboxLease
66
66
  export interface LocalDeviceOutboxRenewInput extends LocalDeviceOutboxLeaseInput {
67
67
  leaseMs: number;
68
68
  }
69
+ export interface LocalDeviceOutboxRequeueDeadLettersInput {
70
+ dryRun?: boolean;
71
+ kind?: LocalDeviceOutboxKind;
72
+ limit?: number;
73
+ sourceInstanceId?: string;
74
+ }
75
+ export interface LocalDeviceOutboxRequeueDeadLettersResult {
76
+ matched: number;
77
+ requeued: number;
78
+ }
79
+ export interface LocalDeviceOutboxPruneSentInput {
80
+ dryRun?: boolean;
81
+ keepCount?: number;
82
+ olderThanIso?: string;
83
+ sourceInstanceId?: string;
84
+ }
85
+ export interface LocalDeviceOutboxPruneSentResult {
86
+ matched: number;
87
+ pruned: number;
88
+ }
89
+ export interface LocalDeviceOutboxPageStats {
90
+ freelistPages: number;
91
+ pageCount: number;
92
+ pageSizeBytes: number;
93
+ reclaimableBytes: number;
94
+ }
95
+ export interface LocalDeviceOutboxCompactResult {
96
+ after: LocalDeviceOutboxPageStats;
97
+ before: LocalDeviceOutboxPageStats;
98
+ reclaimedBytes: number;
99
+ }
100
+ export interface LocalDeviceOutboxDeadLetterErrorClass {
101
+ count: number;
102
+ error_class: string;
103
+ }
104
+ export interface LocalDeviceOutboxDeadLetterErrorSummary {
105
+ dead_letter_count: number;
106
+ null_error_count: number;
107
+ top_classes: LocalDeviceOutboxDeadLetterErrorClass[];
108
+ }
109
+ export interface LocalDeviceOutboxDeadLetterErrorSummaryInput {
110
+ limit?: number;
111
+ sourceInstanceId?: string;
112
+ }
69
113
  export declare class LocalDeviceOutbox {
70
114
  #private;
71
115
  constructor(options: LocalDeviceOutboxOptions);
@@ -84,6 +128,12 @@ export declare class LocalDeviceOutbox {
84
128
  }): number;
85
129
  get(id: string): LocalDeviceOutboxItem | null;
86
130
  deleteSucceeded(id: string): boolean;
131
+ backupTo(path: string): void;
132
+ requeueDeadLetters(input?: LocalDeviceOutboxRequeueDeadLettersInput): LocalDeviceOutboxRequeueDeadLettersResult;
133
+ pruneSent(input?: LocalDeviceOutboxPruneSentInput): LocalDeviceOutboxPruneSentResult;
134
+ countNonSucceeded(): number;
135
+ pageStats(): LocalDeviceOutboxPageStats;
136
+ compact(): LocalDeviceOutboxCompactResult;
87
137
  hasNonSucceededWork(input: {
88
138
  excludeKinds?: readonly LocalDeviceOutboxKind[];
89
139
  kinds?: readonly LocalDeviceOutboxKind[];
@@ -111,5 +161,14 @@ export declare class LocalDeviceOutbox {
111
161
  summary(input?: {
112
162
  sourceInstanceId?: string;
113
163
  }): LocalDeviceOutboxSummary;
164
+ hasObservedStream(input: {
165
+ sourceInstanceId: string;
166
+ stream: string;
167
+ }): boolean | null;
168
+ countRecordBatches(input: {
169
+ sourceInstanceId: string;
170
+ }): number;
171
+ deadLetterErrorSummary(input?: LocalDeviceOutboxDeadLetterErrorSummaryInput): LocalDeviceOutboxDeadLetterErrorSummary;
114
172
  }
115
173
  export declare function buildLocalDeviceOutboxId(input: BuildLocalDeviceOutboxIdInput): string;
174
+ export declare function classifyDeadLetterError(raw: string): string;
@@ -2,7 +2,8 @@ import { mkdirSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { DatabaseSync } from "node:sqlite";
4
4
  import { hashCanonicalJson } from "./local-device-envelope.js";
5
- const CURRENT_SCHEMA_VERSION = 1;
5
+ const CURRENT_SCHEMA_VERSION = 2;
6
+ const LEGACY_COVERAGE_SCAN_BUDGET = 5000;
6
7
  export class LocalDeviceOutbox {
7
8
  #clock;
8
9
  #db;
@@ -67,12 +68,19 @@ export class LocalDeviceOutbox {
67
68
  updated_at
68
69
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
69
70
  .run(row.id, row.source_instance_id, row.kind, row.status, row.payload_json, row.body_hash, row.attempt_count, row.next_attempt_at, row.lease_holder, row.lease_epoch, row.lease_until, row.last_error, row.acknowledged_at, row.created_at, row.updated_at);
71
+ this.#indexObservedStreams(row.id, row.source_instance_id, row.kind, input.payload);
70
72
  const inserted = this.get(row.id);
71
73
  if (!inserted) {
72
74
  throw new Error(`local outbox insert disappeared before readback: ${row.id}`);
73
75
  }
74
76
  return inserted;
75
77
  }
78
+ #indexObservedStreams(outboxId, sourceInstanceId, kind, payload) {
79
+ if (kind !== "record_batch") {
80
+ return;
81
+ }
82
+ this.#backfillObservedStreamIndex(outboxId, sourceInstanceId, distinctRecordStreams(payload));
83
+ }
76
84
  claimReady(input) {
77
85
  const now = this.#now();
78
86
  const leaseUntil = new Date(this.#clock().getTime() + input.leaseMs).toISOString();
@@ -204,6 +212,158 @@ export class LocalDeviceOutbox {
204
212
  const result = this.#db.prepare("DELETE FROM local_device_outbox WHERE id = ? AND status = 'succeeded'").run(id);
205
213
  return Number(result.changes) === 1;
206
214
  }
215
+ backupTo(path) {
216
+ this.#db.exec(`VACUUM INTO ${sqlStringLiteral(path)}`);
217
+ }
218
+ requeueDeadLetters(input = {}) {
219
+ const limit = normalizeLimit(input.limit);
220
+ const { clauses, params } = deadLetterWhere(input);
221
+ const matched = this.#countWhere(clauses, params, limit);
222
+ if (input.dryRun || matched === 0) {
223
+ return { matched, requeued: 0 };
224
+ }
225
+ const now = this.#now();
226
+ const limitSql = limit == null ? "" : " LIMIT ?";
227
+ const selected = this.#db
228
+ .prepare(`SELECT id
229
+ FROM local_device_outbox
230
+ WHERE ${clauses.join(" AND ")}
231
+ ORDER BY rowid${limitSql}`)
232
+ .all(...(limit == null ? params : [...params, limit]));
233
+ const ids = selected.map((row) => {
234
+ if (!isRecord(row) || typeof row.id !== "string") {
235
+ throw new Error("local outbox dead-letter id query returned an invalid row");
236
+ }
237
+ return row.id;
238
+ });
239
+ if (ids.length === 0) {
240
+ return { matched, requeued: 0 };
241
+ }
242
+ this.#db.exec("BEGIN IMMEDIATE");
243
+ try {
244
+ let requeued = 0;
245
+ for (const chunk of chunkArray(ids, SQLITE_IN_CLAUSE_CHUNK)) {
246
+ const result = this.#db
247
+ .prepare(`UPDATE local_device_outbox
248
+ SET status = 'ready',
249
+ attempt_count = 0,
250
+ next_attempt_at = ?,
251
+ lease_holder = NULL,
252
+ lease_until = NULL,
253
+ last_error = NULL,
254
+ updated_at = ?
255
+ WHERE id IN (${chunk.map(() => "?").join(", ")})
256
+ AND status = 'dead_letter'`)
257
+ .run(now, now, ...chunk);
258
+ requeued += Number(result.changes);
259
+ }
260
+ this.#db.exec("COMMIT");
261
+ return { matched, requeued };
262
+ }
263
+ catch (error) {
264
+ this.#db.exec("ROLLBACK");
265
+ throw error;
266
+ }
267
+ }
268
+ pruneSent(input = {}) {
269
+ if (input.keepCount !== undefined && (!Number.isSafeInteger(input.keepCount) || input.keepCount < 0)) {
270
+ throw new Error("pruneSent keepCount must be a non-negative safe integer");
271
+ }
272
+ const clauses = ["status = 'succeeded'"];
273
+ const params = [];
274
+ if (input.sourceInstanceId) {
275
+ clauses.push("source_instance_id = ?");
276
+ params.push(input.sourceInstanceId);
277
+ }
278
+ if (input.olderThanIso) {
279
+ clauses.push("COALESCE(acknowledged_at, updated_at) < ?");
280
+ params.push(input.olderThanIso);
281
+ }
282
+ if (input.keepCount !== undefined) {
283
+ const scopeClause = input.sourceInstanceId
284
+ ? "source_instance_id = ? AND status = 'succeeded'"
285
+ : "status = 'succeeded'";
286
+ const scopeParams = input.sourceInstanceId ? [input.sourceInstanceId] : [];
287
+ clauses.push(`id NOT IN (
288
+ SELECT id FROM local_device_outbox
289
+ WHERE ${scopeClause}
290
+ ORDER BY rowid DESC
291
+ LIMIT ?
292
+ )`);
293
+ params.push(...scopeParams, input.keepCount);
294
+ }
295
+ const whereClause = clauses.join(" AND ");
296
+ const countRow = this.#db
297
+ .prepare(`SELECT COUNT(*) AS total FROM local_device_outbox WHERE ${whereClause}`)
298
+ .get(...params);
299
+ const matched = isRecord(countRow) ? numberFrom(countRow.total) : 0;
300
+ if (input.dryRun !== false || matched === 0) {
301
+ return { matched, pruned: 0 };
302
+ }
303
+ const idRows = this.#db
304
+ .prepare(`SELECT id FROM local_device_outbox WHERE ${whereClause} ORDER BY rowid`)
305
+ .all(...params);
306
+ const ids = idRows.map((row) => {
307
+ if (!isRecord(row) || typeof row.id !== "string") {
308
+ throw new Error("local outbox prune-sent id query returned an invalid row");
309
+ }
310
+ return row.id;
311
+ });
312
+ if (ids.length === 0) {
313
+ return { matched, pruned: 0 };
314
+ }
315
+ this.#db.exec("BEGIN IMMEDIATE");
316
+ try {
317
+ let pruned = 0;
318
+ for (const chunk of chunkArray(ids, SQLITE_IN_CLAUSE_CHUNK)) {
319
+ const placeholders = chunk.map(() => "?").join(", ");
320
+ this.#db.prepare(`DELETE FROM local_device_observed_stream WHERE outbox_id IN (${placeholders})`).run(...chunk);
321
+ const result = this.#db
322
+ .prepare(`DELETE FROM local_device_outbox WHERE id IN (${placeholders}) AND status = 'succeeded'`)
323
+ .run(...chunk);
324
+ pruned += Number(result.changes);
325
+ }
326
+ this.#db.exec("COMMIT");
327
+ return { matched, pruned };
328
+ }
329
+ catch (error) {
330
+ this.#db.exec("ROLLBACK");
331
+ throw error;
332
+ }
333
+ }
334
+ countNonSucceeded() {
335
+ const row = this.#db.prepare("SELECT COUNT(*) AS total FROM local_device_outbox WHERE status != 'succeeded'").get();
336
+ return isRecord(row) ? numberFrom(row.total) : 0;
337
+ }
338
+ pageStats() {
339
+ return this.#readPageStats();
340
+ }
341
+ compact() {
342
+ const before = this.#readPageStats();
343
+ this.#db.exec("VACUUM");
344
+ this.#db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
345
+ const after = this.#readPageStats();
346
+ const reclaimedPages = Math.max(0, before.pageCount - after.pageCount);
347
+ return { after, before, reclaimedBytes: reclaimedPages * before.pageSizeBytes };
348
+ }
349
+ #readPageStats() {
350
+ const pageSizeBytes = this.#pragmaInt("page_size");
351
+ const pageCount = this.#pragmaInt("page_count");
352
+ const freelistPages = this.#pragmaInt("freelist_count");
353
+ return {
354
+ freelistPages,
355
+ pageCount,
356
+ pageSizeBytes,
357
+ reclaimableBytes: freelistPages * pageSizeBytes,
358
+ };
359
+ }
360
+ #pragmaInt(pragma) {
361
+ const row = this.#db.prepare(`PRAGMA ${pragma}`).get();
362
+ if (!isRecord(row)) {
363
+ return 0;
364
+ }
365
+ return numberFrom(row[pragma]);
366
+ }
207
367
  hasNonSucceededWork(input) {
208
368
  const clauses = ["source_instance_id = ?", "status != 'succeeded'"];
209
369
  const params = [input.sourceInstanceId];
@@ -331,6 +491,155 @@ export class LocalDeviceOutbox {
331
491
  }
332
492
  return summary;
333
493
  }
494
+ hasObservedStream(input) {
495
+ const indexed = this.#db
496
+ .prepare(`SELECT 1 AS found
497
+ FROM local_device_observed_stream AS idx
498
+ JOIN local_device_outbox AS o ON o.id = idx.outbox_id
499
+ WHERE idx.source_instance_id = ?
500
+ AND idx.stream = ?
501
+ AND o.status != 'dead_letter'
502
+ LIMIT 1`)
503
+ .get(input.sourceInstanceId, input.stream);
504
+ if (indexed) {
505
+ return true;
506
+ }
507
+ const unindexed = this.#unindexedRecordBatchIds(input.sourceInstanceId, LEGACY_COVERAGE_SCAN_BUDGET + 1);
508
+ if (unindexed.length === 0) {
509
+ return false;
510
+ }
511
+ if (unindexed.length > LEGACY_COVERAGE_SCAN_BUDGET) {
512
+ return null;
513
+ }
514
+ return this.#legacyHasObservedStream(unindexed, input.stream);
515
+ }
516
+ countRecordBatches(input) {
517
+ const row = this.#db
518
+ .prepare(`SELECT COUNT(*) AS total
519
+ FROM local_device_outbox
520
+ WHERE source_instance_id = ?
521
+ AND kind = 'record_batch'
522
+ AND status != 'dead_letter'`)
523
+ .get(input.sourceInstanceId);
524
+ return isRecord(row) ? numberFrom(row.total) : 0;
525
+ }
526
+ #unindexedRecordBatchIds(sourceInstanceId, limit) {
527
+ const rows = this.#db
528
+ .prepare(`SELECT o.id AS id
529
+ FROM local_device_outbox AS o
530
+ WHERE o.source_instance_id = ?
531
+ AND o.kind = 'record_batch'
532
+ AND o.status != 'dead_letter'
533
+ AND NOT EXISTS (
534
+ SELECT 1 FROM local_device_observed_stream AS idx
535
+ WHERE idx.outbox_id = o.id
536
+ )
537
+ ORDER BY o.rowid
538
+ LIMIT ?`)
539
+ .all(sourceInstanceId, limit);
540
+ return rows.map((row) => {
541
+ if (!isRecord(row) || typeof row.id !== "string") {
542
+ throw new Error("local outbox unindexed record-batch query returned an invalid row");
543
+ }
544
+ return row.id;
545
+ });
546
+ }
547
+ #legacyHasObservedStream(outboxIds, stream) {
548
+ let found = false;
549
+ for (const id of outboxIds) {
550
+ const row = this.#db
551
+ .prepare("SELECT payload_json, source_instance_id FROM local_device_outbox WHERE id = ?")
552
+ .get(id);
553
+ if (!isRecord(row) || typeof row.payload_json !== "string" || typeof row.source_instance_id !== "string") {
554
+ continue;
555
+ }
556
+ let payload;
557
+ try {
558
+ payload = JSON.parse(row.payload_json);
559
+ }
560
+ catch {
561
+ continue;
562
+ }
563
+ const streams = distinctRecordStreams(payload);
564
+ this.#backfillObservedStreamIndex(id, row.source_instance_id, streams);
565
+ if (streams.includes(stream)) {
566
+ found = true;
567
+ }
568
+ }
569
+ return found;
570
+ }
571
+ #backfillObservedStreamIndex(outboxId, sourceInstanceId, streams) {
572
+ const insert = this.#db.prepare(`INSERT OR IGNORE INTO local_device_observed_stream (outbox_id, source_instance_id, stream)
573
+ VALUES (?, ?, ?)`);
574
+ if (streams.length === 0) {
575
+ insert.run(outboxId, sourceInstanceId, EMPTY_STREAM_SENTINEL);
576
+ return;
577
+ }
578
+ for (const stream of streams) {
579
+ insert.run(outboxId, sourceInstanceId, stream);
580
+ }
581
+ }
582
+ deadLetterErrorSummary(input = {}) {
583
+ const limit = input.limit && input.limit > 0 ? input.limit : 5;
584
+ const rows = input.sourceInstanceId
585
+ ? this.#db
586
+ .prepare(`SELECT last_error AS last_error, COUNT(*) AS total
587
+ FROM local_device_outbox
588
+ WHERE status = 'dead_letter' AND source_instance_id = ?
589
+ GROUP BY last_error`)
590
+ .all(input.sourceInstanceId)
591
+ : this.#db
592
+ .prepare(`SELECT last_error AS last_error, COUNT(*) AS total
593
+ FROM local_device_outbox
594
+ WHERE status = 'dead_letter'
595
+ GROUP BY last_error`)
596
+ .all();
597
+ const classCounts = new Map();
598
+ let deadLetterCount = 0;
599
+ let nullErrorCount = 0;
600
+ for (const rowLike of rows) {
601
+ if (!isRecord(rowLike)) {
602
+ continue;
603
+ }
604
+ const total = numberFrom(rowLike.total);
605
+ deadLetterCount += total;
606
+ const raw = rowLike.last_error;
607
+ if (typeof raw !== "string" || raw.trim() === "") {
608
+ nullErrorCount += total;
609
+ continue;
610
+ }
611
+ const errorClass = classifyDeadLetterError(raw);
612
+ classCounts.set(errorClass, (classCounts.get(errorClass) ?? 0) + total);
613
+ }
614
+ const top_classes = [...classCounts.entries()]
615
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
616
+ .slice(0, limit)
617
+ .map(([error_class, count]) => ({ count, error_class }));
618
+ return {
619
+ dead_letter_count: deadLetterCount,
620
+ null_error_count: nullErrorCount,
621
+ top_classes,
622
+ };
623
+ }
624
+ #countWhere(clauses, params, limit) {
625
+ if (limit == null) {
626
+ const row = this.#db
627
+ .prepare(`SELECT COUNT(*) AS total FROM local_device_outbox WHERE ${clauses.join(" AND ")}`)
628
+ .get(...params);
629
+ return isRecord(row) ? numberFrom(row.total) : 0;
630
+ }
631
+ const row = this.#db
632
+ .prepare(`SELECT COUNT(*) AS total
633
+ FROM (
634
+ SELECT 1
635
+ FROM local_device_outbox
636
+ WHERE ${clauses.join(" AND ")}
637
+ ORDER BY rowid
638
+ LIMIT ?
639
+ )`)
640
+ .get(...params, limit);
641
+ return isRecord(row) ? numberFrom(row.total) : 0;
642
+ }
334
643
  #initialize() {
335
644
  this.#db.exec(`
336
645
  PRAGMA journal_mode = WAL;
@@ -341,12 +650,11 @@ export class LocalDeviceOutbox {
341
650
  if (version > CURRENT_SCHEMA_VERSION) {
342
651
  throw new Error(`local outbox schema version ${version} is newer than supported version ${CURRENT_SCHEMA_VERSION}`);
343
652
  }
344
- if (version < 1) {
345
- this.#applySchemaV1();
653
+ this.#applySchemaV1();
654
+ this.#applySchemaV2();
655
+ if (version < CURRENT_SCHEMA_VERSION) {
346
656
  this.#db.exec(`PRAGMA user_version = ${CURRENT_SCHEMA_VERSION}`);
347
- return;
348
657
  }
349
- this.#applySchemaV1();
350
658
  }
351
659
  #applySchemaV1() {
352
660
  this.#db.exec(`
@@ -375,6 +683,18 @@ export class LocalDeviceOutbox {
375
683
  ON local_device_outbox (source_instance_id, status);
376
684
  `);
377
685
  }
686
+ #applySchemaV2() {
687
+ this.#db.exec(`
688
+ CREATE TABLE IF NOT EXISTS local_device_observed_stream (
689
+ outbox_id TEXT NOT NULL,
690
+ source_instance_id TEXT NOT NULL,
691
+ stream TEXT NOT NULL,
692
+ PRIMARY KEY (source_instance_id, stream, outbox_id)
693
+ ) WITHOUT ROWID;
694
+ CREATE INDEX IF NOT EXISTS local_device_observed_stream_outbox_idx
695
+ ON local_device_observed_stream (outbox_id);
696
+ `);
697
+ }
378
698
  #selectReady(sourceInstanceId, now, limit, excludeKinds = []) {
379
699
  const kindClause = excludeKinds.length > 0 ? `AND kind NOT IN (${excludeKinds.map(() => "?").join(", ")})` : "";
380
700
  if (sourceInstanceId) {
@@ -418,6 +738,19 @@ export function buildLocalDeviceOutboxId(input) {
418
738
  source_instance_id: input.sourceInstanceId,
419
739
  })}`;
420
740
  }
741
+ const EMPTY_STREAM_SENTINEL = " :pdpp-empty-record-batch";
742
+ function distinctRecordStreams(payload) {
743
+ if (!(isRecord(payload) && Array.isArray(payload.records))) {
744
+ return [];
745
+ }
746
+ const streams = new Set();
747
+ for (const record of payload.records) {
748
+ if (isRecord(record) && typeof record.stream === "string" && record.stream !== "") {
749
+ streams.add(record.stream);
750
+ }
751
+ }
752
+ return [...streams];
753
+ }
421
754
  function rowToItem(rowLike) {
422
755
  const row = asOutboxRow(rowLike);
423
756
  return {
@@ -444,6 +777,62 @@ function assertOneChange(changes, message) {
444
777
  throw new Error(message);
445
778
  }
446
779
  }
780
+ function deadLetterWhere(input) {
781
+ const clauses = ["status = 'dead_letter'"];
782
+ const params = [];
783
+ if (input.sourceInstanceId) {
784
+ clauses.push("source_instance_id = ?");
785
+ params.push(input.sourceInstanceId);
786
+ }
787
+ if (input.kind) {
788
+ clauses.push("kind = ?");
789
+ params.push(input.kind);
790
+ }
791
+ return { clauses, params };
792
+ }
793
+ function normalizeLimit(value) {
794
+ if (value == null) {
795
+ return null;
796
+ }
797
+ if (!Number.isSafeInteger(value) || value <= 0) {
798
+ throw new Error("dead-letter requeue limit must be a positive safe integer");
799
+ }
800
+ return value;
801
+ }
802
+ function sqlStringLiteral(value) {
803
+ return `'${value.replaceAll("'", "''")}'`;
804
+ }
805
+ const SQLITE_IN_CLAUSE_CHUNK = 500;
806
+ function chunkArray(items, size) {
807
+ const chunks = [];
808
+ for (let i = 0; i < items.length; i += size) {
809
+ chunks.push(items.slice(i, i + size));
810
+ }
811
+ return chunks;
812
+ }
813
+ const DEAD_LETTER_SECRET_RE = /\b(authorization|bearer|token|password|passwd|cookie|secret|otp|api[_-]?key)\b\s*[:=]\s*\S+/gi;
814
+ const DEAD_LETTER_OTP_RE = /\b\d{6}\b/g;
815
+ const DEAD_LETTER_LONG_OPAQUE_RE = /\b[A-Za-z0-9_-]{24,}\b/g;
816
+ const DEAD_LETTER_PATH_RE = /(?:\/home|\/Users|\/root)\/[^\s"',)]+|[A-Za-z]:\\Users\\[^\s"',)]+/g;
817
+ const DEAD_LETTER_URL_RE = /https?:\/\/[^\s"',)]+/gi;
818
+ const DEAD_LETTER_HEX_ID_RE = /\b[0-9a-f]{8,}\b/gi;
819
+ const DEAD_LETTER_NUMBER_RE = /\b\d{4,}\b/g;
820
+ const DEAD_LETTER_MAX_LENGTH = 160;
821
+ export function classifyDeadLetterError(raw) {
822
+ let s = raw.split("\n", 1)[0] ?? "";
823
+ s = s.replace(DEAD_LETTER_PATH_RE, "[PATH]");
824
+ s = s.replace(DEAD_LETTER_URL_RE, "[URL]");
825
+ s = s.replace(DEAD_LETTER_SECRET_RE, (_match, marker) => `${marker}=[REDACTED]`);
826
+ s = s.replace(DEAD_LETTER_OTP_RE, "[REDACTED_OTP]");
827
+ s = s.replace(DEAD_LETTER_LONG_OPAQUE_RE, "[REDACTED]");
828
+ s = s.replace(DEAD_LETTER_HEX_ID_RE, "[ID]");
829
+ s = s.replace(DEAD_LETTER_NUMBER_RE, "[N]");
830
+ s = s.replace(/\s+/g, " ").trim();
831
+ if (s === "") {
832
+ return "(unclassified)";
833
+ }
834
+ return s.length > DEAD_LETTER_MAX_LENGTH ? `${s.slice(0, DEAD_LETTER_MAX_LENGTH - 1)}…` : s;
835
+ }
447
836
  function asOutboxRow(row) {
448
837
  if (!isRecord(row)) {
449
838
  throw new Error("local outbox query returned a non-object row");
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { statSync } from "node:fs";
3
3
  import { readdir, stat } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
+ import { openFingerprintCursor } from "./fingerprint-cursor.js";
5
6
  function pathHash(tool, relativePath) {
6
7
  return createHash("sha256").update(`${tool}:${relativePath}`).digest("hex");
7
8
  }
@@ -54,7 +55,7 @@ export async function buildLocalSourceInventory(tool, sourceHome, stores) {
54
55
  const pathMeta = await statKind(fullPath);
55
56
  const status = coverageStatus(store.classification, pathMeta.exists);
56
57
  coverage.push({
57
- id: `${store.store}:${status}`,
58
+ id: `coverage:${store.store}`,
58
59
  store: store.store,
59
60
  stream: store.stream,
60
61
  status,
@@ -117,3 +118,9 @@ export async function listDirectoryInventory(input) {
117
118
  }
118
119
  return records;
119
120
  }
121
+ export const INVENTORY_FINGERPRINT_EXCLUDE_KEYS = ["mtime_epoch", "size_bytes"];
122
+ export function openInventoryFingerprintCursor(priorState) {
123
+ return openFingerprintCursor(priorState, {
124
+ excludeFromFingerprint: INVENTORY_FINGERPRINT_EXCLUDE_KEYS,
125
+ });
126
+ }
@@ -1,11 +1,12 @@
1
1
  export { COLLECTOR_PROTOCOL_HEADER, COLLECTOR_PROTOCOL_VERSION } from "../collector-protocol.js";
2
- export { buildCollectorStartMessage, type CollectorChildContext, type CollectorConnectorSpec, type CollectorEnrollmentConfig, type CollectorRunConfig, type CollectorRunResult, CollectorStateReadError, drainCollectorQueue, enrollCollector, runCollectorConnector, transformRecordsToCollectorEnvelopes, } from "../collector-runner.js";
2
+ export { buildCollectorStartMessage, COLLECTOR_COVERAGE_STATUSES, type CollectorChildContext, type CollectorCompletenessSummary, type CollectorConnectorSpec, type CollectorCoverageStatus, type CollectorEnrollmentConfig, type CollectorRunConfig, type CollectorRunResult, CollectorStateReadError, deriveLocalCollectorLifecycleState, drainCollectorQueue, enrollCollector, LOCAL_COLLECTOR_LIFECYCLE_STATES, type LocalCollectorLifecycleInput, type LocalCollectorLifecycleState, runCollectorConnector, summarizeCollectorCompleteness, transformRecordsToCollectorEnvelopes, } from "../collector-runner.js";
3
3
  export type { AssistanceAttachment, AssistanceAttachmentKind, AssistanceCompletion, AssistanceCompletionStatus, AssistanceOwnerAction, AssistanceProgressPosture, AssistanceRequest, AssistanceResponseContract, AssistanceSensitivity, DetailCoverageMessage, DetailGapMessage, DetailGapRecoveredMessage, DetailGapStartEntry, EmittedMessage, InteractionKind, InteractionRequest, InteractionResponse, RecordData, StartMessage, StreamScope, ValidateRecord, } from "../connector-runtime-protocol.js";
4
4
  export { isMainModule } from "../is-main-module.js";
5
- export { type EnrollmentExchangeRequest, type EnrollmentExchangeResponse, type GetSourceInstanceStateRequest, type HeartbeatRequest, type IngestBatchRequest, LOCAL_DEVICE_ENDPOINTS, LocalDeviceClient, type LocalDeviceClientOptions, LocalDeviceHttpError, type PutSourceInstanceStateRequest, type SourceInstanceStateResponse, } from "../local-device-client.js";
5
+ export { DEFAULT_LOCAL_DEVICE_REQUEST_TIMEOUT_MS, type EnrollmentExchangeRequest, type EnrollmentExchangeResponse, type GetSourceInstanceStateRequest, type HeartbeatRequest, type IngestBatchRequest, LOCAL_DEVICE_ENDPOINTS, LocalDeviceClient, type LocalDeviceClientOptions, LocalDeviceHttpError, LocalDeviceRequestTimeoutError, type PutSourceInstanceStateRequest, type SourceInstanceStateResponse, } from "../local-device-client.js";
6
6
  export { type BuildLocalDeviceRecordEnvelopeInput, buildLocalDeviceRecordEnvelope, canonicalJson, hashCanonicalJson, type LocalDeviceRecordEnvelope, } from "../local-device-envelope.js";
7
- export { type BuildLocalDeviceOutboxIdInput, buildLocalDeviceOutboxId, LocalDeviceOutbox, type LocalDeviceOutboxClaimInput, type LocalDeviceOutboxDeadLetterInput, type LocalDeviceOutboxEnqueueInput, type LocalDeviceOutboxFailInput, type LocalDeviceOutboxItem, type LocalDeviceOutboxKind, type LocalDeviceOutboxLeaseInput, type LocalDeviceOutboxOptions, type LocalDeviceOutboxRenewInput, type LocalDeviceOutboxStatus, type LocalDeviceOutboxSummary, } from "../local-device-outbox.js";
7
+ export { type BuildLocalDeviceOutboxIdInput, buildLocalDeviceOutboxId, classifyDeadLetterError, LocalDeviceOutbox, type LocalDeviceOutboxClaimInput, type LocalDeviceOutboxCompactResult, type LocalDeviceOutboxDeadLetterErrorClass, type LocalDeviceOutboxDeadLetterErrorSummary, type LocalDeviceOutboxDeadLetterErrorSummaryInput, type LocalDeviceOutboxDeadLetterInput, type LocalDeviceOutboxEnqueueInput, type LocalDeviceOutboxFailInput, type LocalDeviceOutboxItem, type LocalDeviceOutboxKind, type LocalDeviceOutboxLeaseInput, type LocalDeviceOutboxOptions, type LocalDeviceOutboxPageStats, type LocalDeviceOutboxPruneSentInput, type LocalDeviceOutboxPruneSentResult, type LocalDeviceOutboxRenewInput, type LocalDeviceOutboxRequeueDeadLettersInput, type LocalDeviceOutboxRequeueDeadLettersResult, type LocalDeviceOutboxStatus, type LocalDeviceOutboxSummary, } from "../local-device-outbox.js";
8
8
  export { LocalDeviceQueue, type LocalDeviceQueueItem, type LocalDeviceQueueOptions, type LocalDeviceQueueStatus, } from "../local-device-queue.js";
9
9
  export { assertPlacementOrThrow, COLLECTOR_RUNTIME_CAPABILITIES, type ConnectorPlacementInput, type ConnectorRuntimeRequirements, diffRequiredBindings, evaluatePlacement, type PlacementDecision, PROVIDER_RUNTIME_CAPABILITIES, RUNTIME_CAPABILITY_MISMATCH_CODE, type RuntimeBindingName, RuntimeCapabilityMismatchError, type RuntimeCapabilityProfile, } from "../runtime-capabilities.js";
10
10
  export { emitToStdout, parseJsonlLine, stringifyForJsonl } from "../safe-emit.js";
11
11
  export { type EmitGate, type EmitGateRecord, type EmitTombstonesArgs, emitTombstones, type MakeEmitGateOptions, makeEmitGate, passesResourceFilter, passesTimeRange, type RequireCredentialsOrAskArgs, requireCredentialsOrAsk, resourceSet, type StreamRequest, type TimeRange, } from "../scope-filters.js";
12
+ export { buildConnectionScopedSecretEnv, isStaticSecretConnector, type RecoveredStaticSecret, STATIC_SECRET_CONNECTOR_REGISTRY, type StaticSecretCredentialKind, StaticSecretInjectionError, } from "../static-secret-injection.js";
@@ -1,10 +1,11 @@
1
1
  export { COLLECTOR_PROTOCOL_HEADER, COLLECTOR_PROTOCOL_VERSION } from "../collector-protocol.js";
2
- export { buildCollectorStartMessage, CollectorStateReadError, drainCollectorQueue, enrollCollector, runCollectorConnector, transformRecordsToCollectorEnvelopes, } from "../collector-runner.js";
2
+ export { buildCollectorStartMessage, COLLECTOR_COVERAGE_STATUSES, CollectorStateReadError, deriveLocalCollectorLifecycleState, drainCollectorQueue, enrollCollector, LOCAL_COLLECTOR_LIFECYCLE_STATES, runCollectorConnector, summarizeCollectorCompleteness, transformRecordsToCollectorEnvelopes, } from "../collector-runner.js";
3
3
  export { isMainModule } from "../is-main-module.js";
4
- export { LOCAL_DEVICE_ENDPOINTS, LocalDeviceClient, LocalDeviceHttpError, } from "../local-device-client.js";
4
+ export { DEFAULT_LOCAL_DEVICE_REQUEST_TIMEOUT_MS, LOCAL_DEVICE_ENDPOINTS, LocalDeviceClient, LocalDeviceHttpError, LocalDeviceRequestTimeoutError, } from "../local-device-client.js";
5
5
  export { buildLocalDeviceRecordEnvelope, canonicalJson, hashCanonicalJson, } from "../local-device-envelope.js";
6
- export { buildLocalDeviceOutboxId, LocalDeviceOutbox, } from "../local-device-outbox.js";
6
+ export { buildLocalDeviceOutboxId, classifyDeadLetterError, LocalDeviceOutbox, } from "../local-device-outbox.js";
7
7
  export { LocalDeviceQueue, } from "../local-device-queue.js";
8
8
  export { assertPlacementOrThrow, COLLECTOR_RUNTIME_CAPABILITIES, diffRequiredBindings, evaluatePlacement, PROVIDER_RUNTIME_CAPABILITIES, RUNTIME_CAPABILITY_MISMATCH_CODE, RuntimeCapabilityMismatchError, } from "../runtime-capabilities.js";
9
9
  export { emitToStdout, parseJsonlLine, stringifyForJsonl } from "../safe-emit.js";
10
10
  export { emitTombstones, makeEmitGate, passesResourceFilter, passesTimeRange, requireCredentialsOrAsk, resourceSet, } from "../scope-filters.js";
11
+ export { buildConnectionScopedSecretEnv, isStaticSecretConnector, STATIC_SECRET_CONNECTOR_REGISTRY, StaticSecretInjectionError, } from "../static-secret-injection.js";
@@ -23,6 +23,19 @@ function isForbiddenCodePoint(codeUnit) {
23
23
  }
24
24
  return false;
25
25
  }
26
+ export function stripForbiddenControlChars(text) {
27
+ if (checkStringForForbidden(text).isSafe) {
28
+ return text;
29
+ }
30
+ let out = "";
31
+ for (let i = 0; i < text.length; i++) {
32
+ const codeUnit = text.charCodeAt(i);
33
+ if (!isForbiddenCodePoint(codeUnit)) {
34
+ out += text[i];
35
+ }
36
+ }
37
+ return out;
38
+ }
26
39
  function checkStringForForbidden(value) {
27
40
  for (let i = 0; i < value.length; i++) {
28
41
  const codeUnit = value.charCodeAt(i);