@agentstrack/collector 0.2.0 → 0.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +177 -1
  2. package/README.md +78 -28
  3. package/dist/adapters/claude.d.ts +18 -0
  4. package/dist/adapters/claude.js +153 -45
  5. package/dist/adapters/claude.js.map +1 -1
  6. package/dist/adapters/codex.d.ts +15 -1
  7. package/dist/adapters/codex.js +81 -33
  8. package/dist/adapters/codex.js.map +1 -1
  9. package/dist/adapters/opencode.d.ts +19 -7
  10. package/dist/adapters/opencode.js +88 -29
  11. package/dist/adapters/opencode.js.map +1 -1
  12. package/dist/adapters/types.d.ts +16 -0
  13. package/dist/adapters/types.js +61 -0
  14. package/dist/adapters/types.js.map +1 -1
  15. package/dist/cli.js +165 -34
  16. package/dist/cli.js.map +1 -1
  17. package/dist/commands/service.js +40 -10
  18. package/dist/commands/service.js.map +1 -1
  19. package/dist/config.d.ts +2 -1
  20. package/dist/config.js +31 -5
  21. package/dist/config.js.map +1 -1
  22. package/dist/daemon.d.ts +73 -21
  23. package/dist/daemon.js +358 -121
  24. package/dist/daemon.js.map +1 -1
  25. package/dist/git/commits.d.ts +7 -1
  26. package/dist/git/commits.js +36 -17
  27. package/dist/git/commits.js.map +1 -1
  28. package/dist/git/repo.d.ts +13 -4
  29. package/dist/git/repo.js +34 -20
  30. package/dist/git/repo.js.map +1 -1
  31. package/dist/machine.d.ts +27 -0
  32. package/dist/machine.js +46 -0
  33. package/dist/machine.js.map +1 -0
  34. package/dist/privacy/pipeline.d.ts +3 -0
  35. package/dist/privacy/pipeline.js +4 -2
  36. package/dist/privacy/pipeline.js.map +1 -1
  37. package/dist/privacy/redact.d.ts +10 -1
  38. package/dist/privacy/redact.js +29 -3
  39. package/dist/privacy/redact.js.map +1 -1
  40. package/dist/queue/event-id.d.ts +9 -0
  41. package/dist/queue/event-id.js +15 -0
  42. package/dist/queue/event-id.js.map +1 -0
  43. package/dist/queue/spool.d.ts +24 -5
  44. package/dist/queue/spool.js +89 -33
  45. package/dist/queue/spool.js.map +1 -1
  46. package/dist/queue/tailer.d.ts +27 -4
  47. package/dist/queue/tailer.js +89 -28
  48. package/dist/queue/tailer.js.map +1 -1
  49. package/dist/transport/client.d.ts +37 -13
  50. package/dist/transport/client.js +50 -3
  51. package/dist/transport/client.js.map +1 -1
  52. package/package.json +2 -2
@@ -1,4 +1,9 @@
1
1
  import type { EventEnvelope } from '../schema.js';
2
+ export interface Checkpoint {
3
+ inode: string;
4
+ offset: number;
5
+ size: number;
6
+ }
2
7
  /**
3
8
  * Durable local spool.
4
9
  *
@@ -12,8 +17,25 @@ import type { EventEnvelope } from '../schema.js';
12
17
  */
13
18
  export declare class Spool {
14
19
  private readonly db;
20
+ private readonly stmt;
21
+ /**
22
+ * Every checkpoint, in memory. A steady-state scan touches every tracked
23
+ * file every 5s and almost none of them changed; answering that from a Map
24
+ * costs nothing, and only a change is written through.
25
+ */
26
+ private readonly checkpoints;
15
27
  constructor(path: string);
28
+ private loadCheckpoints;
16
29
  private migrate;
30
+ /**
31
+ * A drained backfill leaves the file at its high-water mark, nearly all of
32
+ * it free pages (a 206MB spool holding 78 events was observed). VACUUM once
33
+ * at open when the freelist is both large and the majority of the file —
34
+ * never on a small db, where it is pure cost.
35
+ */
36
+ private vacuumIfBloated;
37
+ /** Runs `fn` atomically. A throw rolls back everything written inside it. */
38
+ transaction<T>(fn: () => T): T;
17
39
  enqueue(events: EventEnvelope[]): number;
18
40
  /** Oldest-first so a backlog drains in the order it happened. */
19
41
  peek(limit: number): {
@@ -33,14 +55,11 @@ export declare class Spool {
33
55
  */
34
56
  fail(eventIds: string[], maxAttempts: number): number;
35
57
  depth(): number;
36
- getCheckpoint(path: string): {
37
- inode: string;
38
- offset: number;
39
- size: number;
40
- } | null;
58
+ getCheckpoint(path: string): Checkpoint | null;
41
59
  setCheckpoint(path: string, inode: string, offset: number, size: number): void;
42
60
  getMeta(key: string): string | null;
43
61
  setMeta(key: string, value: string): void;
62
+ deleteMeta(key: string): void;
44
63
  /** Stable per-install id, generated once. */
45
64
  installId(): string;
46
65
  close(): void;
@@ -15,22 +15,54 @@ import { randomUUID } from 'node:crypto';
15
15
  */
16
16
  export class Spool {
17
17
  db;
18
+ stmt;
19
+ /**
20
+ * Every checkpoint, in memory. A steady-state scan touches every tracked
21
+ * file every 5s and almost none of them changed; answering that from a Map
22
+ * costs nothing, and only a change is written through.
23
+ */
24
+ checkpoints = new Map();
18
25
  constructor(path) {
19
26
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
20
27
  this.db = new Database(path);
21
- // The spool holds un-uploaded telemetry; default umask makes it world-readable.
28
+ // WAL so a crash mid-write cannot corrupt the queue.
29
+ this.db.pragma('journal_mode = WAL');
30
+ this.db.pragma('synchronous = NORMAL');
31
+ // The WAL is otherwise only trimmed on close; a backfill leaves it at its
32
+ // high-water mark forever.
33
+ this.db.pragma('journal_size_limit = 8388608');
34
+ // The spool holds un-uploaded telemetry; default umask makes it
35
+ // world-readable. WAL and SHM exist only after the pragma above.
22
36
  for (const suffix of ['', '-wal', '-shm']) {
23
37
  try {
24
38
  chmodSync(`${path}${suffix}`, 0o600);
25
39
  }
26
40
  catch {
27
- // WAL/SHM may not exist yet; the main db is the one that matters.
41
+ // The main db is the one that matters.
28
42
  }
29
43
  }
30
- // WAL so a crash mid-write cannot corrupt the queue.
31
- this.db.pragma('journal_mode = WAL');
32
- this.db.pragma('synchronous = NORMAL');
33
44
  this.migrate();
45
+ this.vacuumIfBloated();
46
+ this.stmt = {
47
+ insert: this.db.prepare('INSERT OR IGNORE INTO events (event_id, body, created_at) VALUES (?, ?, ?)'),
48
+ peek: this.db.prepare('SELECT event_id, body FROM events ORDER BY created_at ASC, rowid ASC LIMIT ?'),
49
+ del: this.db.prepare('DELETE FROM events WHERE event_id = ?'),
50
+ bump: this.db.prepare('UPDATE events SET attempts = attempts + 1 WHERE event_id = ?'),
51
+ drop: this.db.prepare('DELETE FROM events WHERE event_id = ? AND attempts >= ?'),
52
+ count: this.db.prepare('SELECT COUNT(*) AS n FROM events'),
53
+ checkpoint: this.db.prepare(`INSERT INTO checkpoints (path, inode, offset, size) VALUES (?, ?, ?, ?)
54
+ ON CONFLICT(path) DO UPDATE SET inode = excluded.inode, offset = excluded.offset, size = excluded.size`),
55
+ getMeta: this.db.prepare('SELECT value FROM meta WHERE key = ?'),
56
+ setMeta: this.db.prepare('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value'),
57
+ delMeta: this.db.prepare('DELETE FROM meta WHERE key = ?'),
58
+ };
59
+ this.loadCheckpoints();
60
+ }
61
+ loadCheckpoints() {
62
+ const rows = this.db.prepare('SELECT path, inode, offset, size FROM checkpoints').all();
63
+ this.checkpoints.clear();
64
+ for (const { path: p, ...cp } of rows)
65
+ this.checkpoints.set(p, cp);
34
66
  }
35
67
  migrate() {
36
68
  this.db.exec(`
@@ -55,24 +87,50 @@ export class Spool {
55
87
  );
56
88
  `);
57
89
  }
90
+ /**
91
+ * A drained backfill leaves the file at its high-water mark, nearly all of
92
+ * it free pages (a 206MB spool holding 78 events was observed). VACUUM once
93
+ * at open when the freelist is both large and the majority of the file —
94
+ * never on a small db, where it is pure cost.
95
+ */
96
+ vacuumIfBloated() {
97
+ const free = Number(this.db.pragma('freelist_count', { simple: true }));
98
+ const pages = Number(this.db.pragma('page_count', { simple: true }));
99
+ if (free > 2048 && free > pages / 2) {
100
+ try {
101
+ this.db.exec('VACUUM');
102
+ }
103
+ catch {
104
+ // Out of disk or a concurrent reader: the queue still works, just fat.
105
+ }
106
+ }
107
+ }
108
+ /** Runs `fn` atomically. A throw rolls back everything written inside it. */
109
+ transaction(fn) {
110
+ try {
111
+ return this.db.transaction(fn)();
112
+ }
113
+ catch (error) {
114
+ // setCheckpoint() updated the cache before the rollback (or a failed
115
+ // COMMIT — SQLITE_FULL) undid the row; bring it back in line with disk.
116
+ this.loadCheckpoints();
117
+ throw error;
118
+ }
119
+ }
58
120
  enqueue(events) {
59
121
  if (events.length === 0)
60
122
  return 0;
61
- const stmt = this.db.prepare('INSERT OR IGNORE INTO events (event_id, body, created_at) VALUES (?, ?, ?)');
62
123
  const now = Date.now();
63
- const insertAll = this.db.transaction((batch) => {
124
+ return this.transaction(() => {
64
125
  let written = 0;
65
- for (const event of batch)
66
- written += stmt.run(event.event_id, JSON.stringify(event), now).changes;
126
+ for (const event of events)
127
+ written += this.stmt.insert.run(event.event_id, JSON.stringify(event), now).changes;
67
128
  return written;
68
129
  });
69
- return insertAll(events);
70
130
  }
71
131
  /** Oldest-first so a backlog drains in the order it happened. */
72
132
  peek(limit) {
73
- const rows = this.db
74
- .prepare('SELECT event_id, body FROM events ORDER BY created_at ASC, rowid ASC LIMIT ?')
75
- .all(limit);
133
+ const rows = this.stmt.peek.all(limit);
76
134
  return rows.flatMap((row) => {
77
135
  try {
78
136
  return [{ eventId: row.event_id, event: JSON.parse(row.body) }];
@@ -87,8 +145,7 @@ export class Spool {
87
145
  ack(eventIds) {
88
146
  if (eventIds.length === 0)
89
147
  return;
90
- const stmt = this.db.prepare('DELETE FROM events WHERE event_id = ?');
91
- this.db.transaction((ids) => ids.forEach((id) => stmt.run(id)))(eventIds);
148
+ this.transaction(() => eventIds.forEach((id) => this.stmt.del.run(id)));
92
149
  }
93
150
  /**
94
151
  * Records a failed attempt so poison events can be dropped eventually.
@@ -104,37 +161,36 @@ export class Spool {
104
161
  if (eventIds.length === 0)
105
162
  return 0;
106
163
  const limit = Math.max(1, maxAttempts);
107
- const bump = this.db.prepare('UPDATE events SET attempts = attempts + 1 WHERE event_id = ?');
108
- const drop = this.db.prepare('DELETE FROM events WHERE event_id = ? AND attempts >= ?');
109
- return this.db.transaction((ids) => {
164
+ return this.transaction(() => {
110
165
  let dropped = 0;
111
- for (const id of ids) {
112
- bump.run(id);
113
- dropped += drop.run(id, limit).changes;
166
+ for (const id of eventIds) {
167
+ this.stmt.bump.run(id);
168
+ dropped += this.stmt.drop.run(id, limit).changes;
114
169
  }
115
170
  return dropped;
116
- })(eventIds);
171
+ });
117
172
  }
118
173
  depth() {
119
- return this.db.prepare('SELECT COUNT(*) AS n FROM events').get().n;
174
+ return this.stmt.count.get().n;
120
175
  }
121
176
  getCheckpoint(path) {
122
- return (this.db.prepare('SELECT inode, offset, size FROM checkpoints WHERE path = ?').get(path) ?? null);
177
+ return this.checkpoints.get(path) ?? null;
123
178
  }
124
179
  setCheckpoint(path, inode, offset, size) {
125
- this.db
126
- .prepare(`INSERT INTO checkpoints (path, inode, offset, size) VALUES (?, ?, ?, ?)
127
- ON CONFLICT(path) DO UPDATE SET inode = excluded.inode, offset = excluded.offset, size = excluded.size`)
128
- .run(path, inode, offset, size);
180
+ const current = this.checkpoints.get(path);
181
+ if (current && current.inode === inode && current.offset === offset && current.size === size)
182
+ return;
183
+ this.stmt.checkpoint.run(path, inode, offset, size);
184
+ this.checkpoints.set(path, { inode, offset, size });
129
185
  }
130
186
  getMeta(key) {
131
- return (this.db.prepare('SELECT value FROM meta WHERE key = ?').get(key)?.value ??
132
- null);
187
+ return this.stmt.getMeta.get(key)?.value ?? null;
133
188
  }
134
189
  setMeta(key, value) {
135
- this.db
136
- .prepare('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value')
137
- .run(key, value);
190
+ this.stmt.setMeta.run(key, value);
191
+ }
192
+ deleteMeta(key) {
193
+ this.stmt.delMeta.run(key);
138
194
  }
139
195
  /** Stable per-install id, generated once. */
140
196
  installId() {
@@ -1 +1 @@
1
- {"version":3,"file":"spool.js","sourceRoot":"","sources":["../../src/queue/spool.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"spool.js","sourceRoot":"","sources":["../../src/queue/spool.ts"],"names":[],"mappings":"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"}
@@ -2,8 +2,11 @@ import type { Spool } from './spool.js';
2
2
  /**
3
3
  * Resumable line tailer.
4
4
  *
5
- * Checkpoints (path, inode, offset) after every read so a restart continues
6
- * exactly where it stopped. Three failure modes it must survive:
5
+ * Reads in fixed-size chunks and hands every complete line to `onLines`; the
6
+ * checkpoint (path, inode, offset) for a chunk is written in the SAME
7
+ * transaction as whatever `onLines` spooled, so a throw or a crash between
8
+ * "read" and "queued" re-reads those lines instead of losing them. Failure
9
+ * modes it must survive:
7
10
  *
8
11
  * - rotation/replacement: the inode changes, so the old offset is meaningless
9
12
  * and we start from zero.
@@ -14,12 +17,32 @@ import type { Spool } from './spool.js';
14
17
  * remainder arrives on the next pass as an orphaned fragment and both
15
18
  * halves fail to parse. So the checkpoint stops at the last complete
16
19
  * newline and the partial line is re-read whole next time.
20
+ * - a pathological line: anything over `maxLineBytes` is skipped to the next
21
+ * newline and counted, never buffered — one 512MiB line used to throw and
22
+ * abort every later file on every scan.
23
+ * - a huge backlog: at most `maxBytesPerScan` per file per call, so a first
24
+ * import yields to the upload loop instead of reading 27MB files back to
25
+ * back.
17
26
  *
18
27
  * Offsets are byte offsets, computed from the buffer rather than from decoded
19
28
  * strings — a multi-byte character would otherwise desynchronise the position.
20
29
  */
30
+ export interface TailedLine {
31
+ text: string;
32
+ /** Byte offset of the line's first byte. Stable for an append-only file. */
33
+ offset: number;
34
+ }
21
35
  export interface TailResult {
22
- lines: string[];
36
+ lines: number;
23
37
  bytesRead: number;
38
+ /** Lines over `maxLineBytes`, discarded. */
39
+ skipped: number;
40
+ /** True when `maxBytesPerScan` stopped the read before EOF. */
41
+ more: boolean;
42
+ }
43
+ export interface TailOptions {
44
+ chunkBytes?: number;
45
+ maxLineBytes?: number;
46
+ maxBytesPerScan?: number;
24
47
  }
25
- export declare function tailFile(path: string, spool: Spool): Promise<TailResult>;
48
+ export declare function tailFile(path: string, spool: Spool, onLines: (lines: TailedLine[]) => void, options?: TailOptions): Promise<TailResult>;
@@ -1,10 +1,16 @@
1
- import { existsSync, statSync } from 'node:fs';
1
+ import { statSync } from 'node:fs';
2
2
  import { open } from 'node:fs/promises';
3
3
  const NEWLINE = 0x0a;
4
- export async function tailFile(path, spool) {
5
- if (!existsSync(path))
6
- return { lines: [], bytesRead: 0 };
7
- const stats = statSync(path);
4
+ const CR = 0x0d;
5
+ const EMPTY = Buffer.alloc(0);
6
+ export async function tailFile(path, spool, onLines, options = {}) {
7
+ const chunkBytes = options.chunkBytes ?? 4 * 1024 * 1024;
8
+ const maxLineBytes = options.maxLineBytes ?? 8 * 1024 * 1024;
9
+ const maxBytesPerScan = options.maxBytesPerScan ?? 64 * 1024 * 1024;
10
+ const result = { lines: 0, bytesRead: 0, skipped: 0, more: false };
11
+ const stats = statSync(path, { throwIfNoEntry: false });
12
+ if (!stats)
13
+ return result;
8
14
  const inode = String(stats.ino);
9
15
  const checkpoint = spool.getCheckpoint(path);
10
16
  let start = 0;
@@ -14,35 +20,90 @@ export async function tailFile(path, spool) {
14
20
  // else: rotated (inode differs) or truncated (offset > size) — reread from 0.
15
21
  if (start >= stats.size) {
16
22
  spool.setCheckpoint(path, inode, stats.size, stats.size);
17
- return { lines: [], bytesRead: 0 };
23
+ return result;
18
24
  }
19
- const length = stats.size - start;
20
- const buffer = Buffer.allocUnsafe(length);
21
25
  const handle = await open(path, 'r');
22
26
  try {
23
- await handle.read(buffer, 0, length, start);
27
+ let position = start;
28
+ let carry = EMPTY;
29
+ // Inside a line that already exceeded maxLineBytes: drop bytes up to and
30
+ // including the next newline.
31
+ let skipping = false;
32
+ while (position < stats.size && result.bytesRead < maxBytesPerScan) {
33
+ const want = Math.min(chunkBytes, stats.size - position);
34
+ const chunk = Buffer.allocUnsafe(want);
35
+ let got = 0;
36
+ // read() may return short; loop until the chunk is full or the file
37
+ // turned out shorter than stat said (truncated under us).
38
+ while (got < want) {
39
+ const { bytesRead } = await handle.read(chunk, got, want - got, position + got);
40
+ if (bytesRead === 0)
41
+ break;
42
+ got += bytesRead;
43
+ }
44
+ if (got === 0)
45
+ break;
46
+ result.bytesRead += got;
47
+ const data = carry.length ? Buffer.concat([carry, chunk.subarray(0, got)]) : chunk.subarray(0, got);
48
+ const dataStart = position - carry.length;
49
+ position += got;
50
+ const lastNewline = data.lastIndexOf(NEWLINE);
51
+ if (lastNewline === -1) {
52
+ // No complete line in what we have. Either keep waiting for the
53
+ // newline or, past the cap, give up on this line entirely.
54
+ if (skipping || data.length > maxLineBytes) {
55
+ if (!skipping)
56
+ result.skipped += 1;
57
+ skipping = true;
58
+ carry = EMPTY;
59
+ spool.setCheckpoint(path, inode, position, stats.size);
60
+ }
61
+ else {
62
+ carry = Buffer.from(data);
63
+ }
64
+ continue;
65
+ }
66
+ const lines = [];
67
+ let lineStart = 0;
68
+ while (lineStart <= lastNewline) {
69
+ const nl = data.indexOf(NEWLINE, lineStart);
70
+ const end = nl > lineStart && data[nl - 1] === CR ? nl - 1 : nl;
71
+ if (skipping) {
72
+ skipping = false; // this newline terminates the oversized line
73
+ }
74
+ else if (nl - lineStart > maxLineBytes) {
75
+ result.skipped += 1;
76
+ }
77
+ else if (end > lineStart) {
78
+ lines.push({ text: data.toString('utf8', lineStart, end), offset: dataStart + lineStart });
79
+ }
80
+ lineStart = nl + 1;
81
+ }
82
+ const consumed = dataStart + lastNewline + 1;
83
+ // Lines and their checkpoint commit together, or not at all.
84
+ spool.transaction(() => {
85
+ if (lines.length > 0)
86
+ onLines(lines);
87
+ spool.setCheckpoint(path, inode, consumed, stats.size);
88
+ });
89
+ result.lines += lines.length;
90
+ const rest = data.subarray(lastNewline + 1);
91
+ if (rest.length > maxLineBytes) {
92
+ result.skipped += 1;
93
+ skipping = true;
94
+ carry = EMPTY;
95
+ spool.setCheckpoint(path, inode, position, stats.size);
96
+ }
97
+ else {
98
+ // Copy, so the 4MB chunk behind the slice can be collected.
99
+ carry = rest.length ? Buffer.from(rest) : EMPTY;
100
+ }
101
+ }
102
+ result.more = position < stats.size;
24
103
  }
25
104
  finally {
26
105
  await handle.close();
27
106
  }
28
- // Consume only up to the final newline. Anything after it is a line the
29
- // agent has not finished writing.
30
- const lastNewline = buffer.lastIndexOf(NEWLINE);
31
- if (lastNewline === -1) {
32
- // No complete line yet — leave the checkpoint untouched so the whole
33
- // partial line is re-read once it is terminated.
34
- return { lines: [], bytesRead: 0 };
35
- }
36
- const consumable = buffer.subarray(0, lastNewline + 1);
37
- const lines = consumable
38
- .toString('utf8')
39
- .split('\n')
40
- // The trailing element after the final \n is always '' — drop it, and drop
41
- // any blank lines rather than handing '' to a parser.
42
- .filter((line) => line.length > 0)
43
- .map((line) => (line.endsWith('\r') ? line.slice(0, -1) : line));
44
- const consumed = start + consumable.length;
45
- spool.setCheckpoint(path, inode, consumed, stats.size);
46
- return { lines, bytesRead: consumable.length };
107
+ return result;
47
108
  }
48
109
  //# sourceMappingURL=tailer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tailer.js","sourceRoot":"","sources":["../../src/queue/tailer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AA2BxC,MAAM,OAAO,GAAG,IAAI,CAAC;AAErB,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAY,EAAE,KAAY;IACvD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;IAE1D,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC7B,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAChC,MAAM,UAAU,GAAG,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;IAE7C,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,UAAU,IAAI,UAAU,CAAC,KAAK,KAAK,KAAK,IAAI,UAAU,CAAC,MAAM,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QAChF,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC;IAC5B,CAAC;IACD,8EAA8E;IAE9E,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QACxB,KAAK,CAAC,aAAa,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACzD,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;IACrC,CAAC;IAED,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC;IAClC,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAE1C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACrC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;IAC9C,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;IAED,wEAAwE;IACxE,kCAAkC;IAClC,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAChD,IAAI,WAAW,KAAK,CAAC,CAAC,EAAE,CAAC;QACvB,qEAAqE;QACrE,iDAAiD;QACjD,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;IACrC,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,WAAW,GAAG,CAAC,CAAC,CAAC;IACvD,MAAM,KAAK,GAAG,UAAU;SACrB,QAAQ,CAAC,MAAM,CAAC;SAChB,KAAK,CAAC,IAAI,CAAC;QACZ,2EAA2E;QAC3E,sDAAsD;SACrD,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;SACjC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAEnE,MAAM,QAAQ,GAAG,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC;IAC3C,KAAK,CAAC,aAAa,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IACvD,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC;AACjD,CAAC"}
1
+ {"version":3,"file":"tailer.js","sourceRoot":"","sources":["../../src/queue/tailer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAoDxC,MAAM,OAAO,GAAG,IAAI,CAAC;AACrB,MAAM,EAAE,GAAG,IAAI,CAAC;AAChB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAE9B,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,IAAY,EACZ,KAAY,EACZ,OAAsC,EACtC,UAAuB,EAAE;IAEzB,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;IACzD,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;IAC7D,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,IAAI,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;IACpE,MAAM,MAAM,GAAe,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAE/E,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,CAAC;IACxD,IAAI,CAAC,KAAK;QAAE,OAAO,MAAM,CAAC;IAC1B,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAChC,MAAM,UAAU,GAAG,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;IAE7C,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,UAAU,IAAI,UAAU,CAAC,KAAK,KAAK,KAAK,IAAI,UAAU,CAAC,MAAM,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QAChF,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC;IAC5B,CAAC;IACD,8EAA8E;IAE9E,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QACxB,KAAK,CAAC,aAAa,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACzD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACrC,IAAI,CAAC;QACH,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,IAAI,KAAK,GAAG,KAAK,CAAC;QAClB,yEAAyE;QACzE,8BAA8B;QAC9B,IAAI,QAAQ,GAAG,KAAK,CAAC;QAErB,OAAO,QAAQ,GAAG,KAAK,CAAC,IAAI,IAAI,MAAM,CAAC,SAAS,GAAG,eAAe,EAAE,CAAC;YACnE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,GAAG,QAAQ,CAAC,CAAC;YACzD,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;YACvC,IAAI,GAAG,GAAG,CAAC,CAAC;YACZ,oEAAoE;YACpE,0DAA0D;YAC1D,OAAO,GAAG,GAAG,IAAI,EAAE,CAAC;gBAClB,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,GAAG,GAAG,EAAE,QAAQ,GAAG,GAAG,CAAC,CAAC;gBAChF,IAAI,SAAS,KAAK,CAAC;oBAAE,MAAM;gBAC3B,GAAG,IAAI,SAAS,CAAC;YACnB,CAAC;YACD,IAAI,GAAG,KAAK,CAAC;gBAAE,MAAM;YACrB,MAAM,CAAC,SAAS,IAAI,GAAG,CAAC;YAExB,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YACpG,MAAM,SAAS,GAAG,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC;YAC1C,QAAQ,IAAI,GAAG,CAAC;YAEhB,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;YAC9C,IAAI,WAAW,KAAK,CAAC,CAAC,EAAE,CAAC;gBACvB,gEAAgE;gBAChE,2DAA2D;gBAC3D,IAAI,QAAQ,IAAI,IAAI,CAAC,MAAM,GAAG,YAAY,EAAE,CAAC;oBAC3C,IAAI,CAAC,QAAQ;wBAAE,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC;oBACnC,QAAQ,GAAG,IAAI,CAAC;oBAChB,KAAK,GAAG,KAAK,CAAC;oBACd,KAAK,CAAC,aAAa,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;gBACzD,CAAC;qBAAM,CAAC;oBACN,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC5B,CAAC;gBACD,SAAS;YACX,CAAC;YAED,MAAM,KAAK,GAAiB,EAAE,CAAC;YAC/B,IAAI,SAAS,GAAG,CAAC,CAAC;YAClB,OAAO,SAAS,IAAI,WAAW,EAAE,CAAC;gBAChC,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;gBAC5C,MAAM,GAAG,GAAG,EAAE,GAAG,SAAS,IAAI,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBAChE,IAAI,QAAQ,EAAE,CAAC;oBACb,QAAQ,GAAG,KAAK,CAAC,CAAC,6CAA6C;gBACjE,CAAC;qBAAM,IAAI,EAAE,GAAG,SAAS,GAAG,YAAY,EAAE,CAAC;oBACzC,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC;gBACtB,CAAC;qBAAM,IAAI,GAAG,GAAG,SAAS,EAAE,CAAC;oBAC3B,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,SAAS,GAAG,SAAS,EAAE,CAAC,CAAC;gBAC7F,CAAC;gBACD,SAAS,GAAG,EAAE,GAAG,CAAC,CAAC;YACrB,CAAC;YAED,MAAM,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,CAAC,CAAC;YAC7C,6DAA6D;YAC7D,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE;gBACrB,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;oBAAE,OAAO,CAAC,KAAK,CAAC,CAAC;gBACrC,KAAK,CAAC,aAAa,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YACzD,CAAC,CAAC,CAAC;YACH,MAAM,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,CAAC;YAE7B,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC;YAC5C,IAAI,IAAI,CAAC,MAAM,GAAG,YAAY,EAAE,CAAC;gBAC/B,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC;gBACpB,QAAQ,GAAG,IAAI,CAAC;gBAChB,KAAK,GAAG,KAAK,CAAC;gBACd,KAAK,CAAC,aAAa,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YACzD,CAAC;iBAAM,CAAC;gBACN,4DAA4D;gBAC5D,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;YAClD,CAAC;QACH,CAAC;QACD,MAAM,CAAC,IAAI,GAAG,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC;IACtC,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -1,4 +1,8 @@
1
+ import { z } from 'zod';
1
2
  import type { EventEnvelope } from '../schema.js';
3
+ import type { MachineInfo } from '../machine.js';
4
+ /** Read from package.json so the CLI, register and health can never drift from what is published. */
5
+ export declare const VERSION: string;
2
6
  /**
3
7
  * HTTP client for the AgentsTrack API.
4
8
  *
@@ -10,14 +14,25 @@ export interface ClientOptions {
10
14
  apiKey: string;
11
15
  timeoutMs?: number;
12
16
  }
13
- export interface BatchResult {
14
- accepted: number;
15
- duplicates: number;
16
- rejected: {
17
- index: number;
18
- reason: string;
19
- }[];
20
- }
17
+ /**
18
+ * The batch response is a trust boundary: a proxy error page or a newer
19
+ * server shape must fail loudly here, not surface as `undefined.length` deep
20
+ * in the upload policy. `quota` arrives only from servers that meter events.
21
+ */
22
+ export declare const BatchResult: z.ZodObject<{
23
+ accepted: z.ZodNumber;
24
+ duplicates: z.ZodNumber;
25
+ rejected: z.ZodArray<z.ZodObject<{
26
+ index: z.ZodNumber;
27
+ reason: z.ZodString;
28
+ }, z.core.$strip>>;
29
+ quota: z.ZodOptional<z.ZodObject<{
30
+ limit: z.ZodNullable<z.ZodNumber>;
31
+ used: z.ZodNullable<z.ZodNumber>;
32
+ exceeded: z.ZodBoolean;
33
+ }, z.core.$strip>>;
34
+ }, z.core.$strip>;
35
+ export type BatchResult = z.infer<typeof BatchResult>;
21
36
  export interface ServerConfig {
22
37
  privacy_mode: 'metadata' | 'analytics' | 'full';
23
38
  idle_timeout_seconds: number;
@@ -33,18 +48,25 @@ export declare class ApiError extends Error {
33
48
  readonly status: number;
34
49
  /** Whether trying again later could succeed. */
35
50
  readonly retryable: boolean;
51
+ /** Server-requested minimum wait before the next attempt, from Retry-After. */
52
+ readonly retryAfterMs?: number | undefined;
36
53
  constructor(message: string, status: number,
37
54
  /** Whether trying again later could succeed. */
38
- retryable: boolean);
55
+ retryable: boolean,
56
+ /** Server-requested minimum wait before the next attempt, from Retry-After. */
57
+ retryAfterMs?: number | undefined);
39
58
  }
40
59
  export declare class ApiClient {
41
60
  private readonly options;
42
61
  constructor(options: ClientOptions);
43
- registerCollector(input: {
62
+ /**
63
+ * Register and health both carry `MachineInfo`. The server keeps hostname,
64
+ * os, release, arch, machine kind and the request IP for operations and
65
+ * abuse prevention only; none of it is shown in the product.
66
+ */
67
+ registerCollector(input: Partial<MachineInfo> & {
44
68
  hostname: string;
45
69
  label?: string;
46
- os?: string;
47
- arch?: string;
48
70
  version?: string;
49
71
  /** The mode this device will actually enforce, which may be stricter than the org's. */
50
72
  privacy_mode?: string;
@@ -58,7 +80,7 @@ export declare class ApiClient {
58
80
  }>;
59
81
  getConfig(): Promise<ServerConfig>;
60
82
  sendBatch(events: EventEnvelope[]): Promise<BatchResult>;
61
- health(input: {
83
+ health(input: Partial<MachineInfo> & {
62
84
  collector_id: string;
63
85
  queue_depth: number;
64
86
  version?: string;
@@ -72,5 +94,7 @@ export declare class ApiClient {
72
94
  }>;
73
95
  private request;
74
96
  }
97
+ /** Retry-After is either delta-seconds or an HTTP date. Unparseable means no request. */
98
+ export declare function retryAfterMs(header: string | null): number | undefined;
75
99
  /** Exponential backoff with jitter, capped. */
76
100
  export declare function backoffMs(attempt: number, baseMs?: number, capMs?: number): number;