@agentstrack/collector 0.2.1 → 0.4.1

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 (55) hide show
  1. package/CHANGELOG.md +209 -1
  2. package/README.md +114 -38
  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 +87 -34
  8. package/dist/adapters/codex.js.map +1 -1
  9. package/dist/adapters/opencode.d.ts +17 -6
  10. package/dist/adapters/opencode.js +72 -26
  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 +164 -33
  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 +1 -1
  20. package/dist/config.js +18 -5
  21. package/dist/config.js.map +1 -1
  22. package/dist/daemon.d.ts +73 -21
  23. package/dist/daemon.js +350 -119
  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 +6 -0
  35. package/dist/privacy/pipeline.js +41 -7
  36. package/dist/privacy/pipeline.js.map +1 -1
  37. package/dist/privacy/redact.d.ts +15 -2
  38. package/dist/privacy/redact.js +45 -6
  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/sessions/title.d.ts +14 -2
  50. package/dist/sessions/title.js +18 -6
  51. package/dist/sessions/title.js.map +1 -1
  52. package/dist/transport/client.d.ts +37 -13
  53. package/dist/transport/client.js +50 -3
  54. package/dist/transport/client.js.map +1 -1
  55. package/package.json +2 -2
@@ -1,3 +1,5 @@
1
+ /** Org rules run on at most this many characters; built-ins see the whole value. */
2
+ export const ORG_SUBJECT_CAP = 64 * 1024;
1
3
  export const BUILTIN_RULES = [
2
4
  { name: 'anthropic_key', pattern: /sk-ant-[A-Za-z0-9_-]{20,}/g, replacement: '[REDACTED:anthropic_key]' },
3
5
  { name: 'openai_key', pattern: /sk-(?:proj-)?[A-Za-z0-9_-]{20,}/g, replacement: '[REDACTED:openai_key]' },
@@ -18,30 +20,67 @@ export const BUILTIN_RULES = [
18
20
  { name: 'inline_password_flag', pattern: /(--password[= ]|(?<![\w-])-p)(?!\s)("[^"]*"|'[^']*'|\S+)/g, replacement: '$1[REDACTED]' },
19
21
  { name: 'generic_hex_secret', pattern: /\b[a-f0-9]{40,}\b/g, replacement: '[REDACTED:hex]' },
20
22
  ];
23
+ /** The one kind reported for any rule that is not built in. */
24
+ export const ORG_RULE_KIND = 'org_rule';
25
+ const BUILTIN_NAMES = new Set(BUILTIN_RULES.map((rule) => rule.name));
26
+ /**
27
+ * An org's own rule name can itself describe the shape of that org's secrets
28
+ * ("acme_prod_db_password"), which is their business and not ours to ship back
29
+ * to the server. Anything not built in therefore reports as one generic kind.
30
+ */
31
+ const reportedKind = (name) => (BUILTIN_NAMES.has(name) ? name : ORG_RULE_KIND);
21
32
  export function redact(input, extraRules = []) {
22
33
  let text = input;
23
- const redactions = [];
34
+ const counts = {};
24
35
  for (const rule of [...BUILTIN_RULES, ...extraRules]) {
36
+ // Org rules see only a bounded prefix; the untouched tail is re-appended.
37
+ const capped = rule.capSubject === true && text.length > ORG_SUBJECT_CAP;
38
+ const subject = capped ? text.slice(0, ORG_SUBJECT_CAP) : text;
25
39
  // Fresh lastIndex per call: these regexes are global and module-level, so
26
40
  // reusing them statefully across calls would skip matches.
27
41
  rule.pattern.lastIndex = 0;
28
- if (!rule.pattern.test(text))
42
+ // `match` on a global regex returns every match, so the tally is a count of
43
+ // matches rather than of rules. Only the length is ever read.
44
+ const hits = subject.match(rule.pattern)?.length ?? 0;
45
+ if (hits === 0)
29
46
  continue;
30
47
  rule.pattern.lastIndex = 0;
31
- text = text.replace(rule.pattern, rule.replacement);
32
- redactions.push(rule.name);
48
+ const replaced = subject.replace(rule.pattern, rule.replacement);
49
+ text = capped ? replaced + text.slice(ORG_SUBJECT_CAP) : replaced;
50
+ const kind = reportedKind(rule.name);
51
+ counts[kind] = (counts[kind] ?? 0) + hits;
33
52
  }
34
- return { text, redactions };
53
+ return { text, redactions: Object.keys(counts), counts };
35
54
  }
36
- /** Compiles org-supplied patterns, skipping any that do not compile. */
55
+ /**
56
+ * An org rule is rejected before compilation when its source is longer than
57
+ * this, contains a nested quantifier, or uses a backreference — all shapes that
58
+ * invite catastrophic backtracking on V8's engine, which runs single-threaded
59
+ * on the daemon and would hang the whole collector.
60
+ */
61
+ const MAX_ORG_PATTERN_LENGTH = 256;
62
+ // A quantified group whose body holds a quantifier, an alternation or an
63
+ // interval — directly or in one nested group. A heuristic: it rejects the
64
+ // common catastrophic shapes, not every regex that can blow up.
65
+ const NESTED_QUANTIFIER = /\((?:[^()]|\([^()]*\))*(?:[+*?|{]|\([^()]*[+*?|{][^()]*\))(?:[^()]|\([^()]*\))*\)[+*?{]/;
66
+ const BACKREFERENCE = /\\[1-9]|\\k</;
67
+ /** Compiles org-supplied patterns, skipping any unsafe or malformed rule. */
37
68
  export function compileRules(rules) {
38
69
  const compiled = [];
39
70
  for (const [index, rule] of rules.entries()) {
71
+ if (rule.pattern.length > MAX_ORG_PATTERN_LENGTH ||
72
+ NESTED_QUANTIFIER.test(rule.pattern) ||
73
+ BACKREFERENCE.test(rule.pattern)) {
74
+ // Same policy as a malformed rule: log-and-skip rather than stop the
75
+ // collector. (Logging happens at the call site, which holds the logger.)
76
+ continue;
77
+ }
40
78
  try {
41
79
  compiled.push({
42
80
  name: `org_rule_${index}`,
43
81
  pattern: new RegExp(rule.pattern, 'g'),
44
82
  replacement: rule.replacement,
83
+ capSubject: true,
45
84
  });
46
85
  }
47
86
  catch {
@@ -1 +1 @@
1
- {"version":3,"file":"redact.js","sourceRoot":"","sources":["../../src/privacy/redact.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"redact.js","sourceRoot":"","sources":["../../src/privacy/redact.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * A stable event_id from a stable seed, so re-reading a transcript (rotated
3
+ * inode, purged spool, second collector on the same files) produces the same
4
+ * id and the server's dedupe absorbs it instead of double-counting.
5
+ *
6
+ * sha256, truncated and stamped as a UUID: version nibble 8 (RFC 9562
7
+ * "custom"), variant bits 10xx — the shape the server's `uuid()` check wants.
8
+ */
9
+ export declare function deterministicEventId(seed: string): string;
@@ -0,0 +1,15 @@
1
+ import { createHash } from 'node:crypto';
2
+ /**
3
+ * A stable event_id from a stable seed, so re-reading a transcript (rotated
4
+ * inode, purged spool, second collector on the same files) produces the same
5
+ * id and the server's dedupe absorbs it instead of double-counting.
6
+ *
7
+ * sha256, truncated and stamped as a UUID: version nibble 8 (RFC 9562
8
+ * "custom"), variant bits 10xx — the shape the server's `uuid()` check wants.
9
+ */
10
+ export function deterministicEventId(seed) {
11
+ const hex = createHash('sha256').update(seed).digest('hex');
12
+ const variant = ((parseInt(hex.charAt(16), 16) & 0x3) | 0x8).toString(16);
13
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-8${hex.slice(13, 16)}-${variant}${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
14
+ }
15
+ //# sourceMappingURL=event-id.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-id.js","sourceRoot":"","sources":["../../src/queue/event-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAC/C,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC5D,MAAM,OAAO,GAAG,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAC1E,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;AAC5H,CAAC"}
@@ -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":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGzC;;;;;;;;;;GAUG;AACH,MAAM,OAAO,KAAK;IACC,EAAE,CAAoB;IAEvC,YAAY,IAAY;QACtB,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC3D,IAAI,CAAC,EAAE,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC7B,gFAAgF;QAChF,KAAK,MAAM,MAAM,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;YAC1C,IAAI,CAAC;gBACH,SAAS,CAAC,GAAG,IAAI,GAAG,MAAM,EAAE,EAAE,KAAK,CAAC,CAAC;YACvC,CAAC;YAAC,MAAM,CAAC;gBACP,kEAAkE;YACpE,CAAC;QACH,CAAC;QACD,qDAAqD;QACrD,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;QACrC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAC;QACvC,IAAI,CAAC,OAAO,EAAE,CAAC;IACjB,CAAC;IAEO,OAAO;QACb,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;KAoBZ,CAAC,CAAC;IACL,CAAC;IAED,OAAO,CAAC,MAAuB;QAC7B,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,CAAC,CAAC;QAClC,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAC1B,4EAA4E,CAC7E,CAAC;QACF,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,SAAS,GAAG,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,KAAsB,EAAE,EAAE;YAC/D,IAAI,OAAO,GAAG,CAAC,CAAC;YAChB,KAAK,MAAM,KAAK,IAAI,KAAK;gBAAE,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC;YACnG,OAAO,OAAO,CAAC;QACjB,CAAC,CAAC,CAAC;QACH,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC;IAC3B,CAAC;IAED,iEAAiE;IACjE,IAAI,CAAC,KAAa;QAChB,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE;aACjB,OAAO,CAAC,8EAA8E,CAAC;aACvF,GAAG,CAAC,KAAK,CAAyC,CAAC;QAEtD,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;YAC1B,IAAI,CAAC;gBACH,OAAO,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAkB,EAAE,CAAC,CAAC;YACnF,CAAC;YAAC,MAAM,CAAC;gBACP,0DAA0D;gBAC1D,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC;gBACzB,OAAO,EAAE,CAAC;YACZ,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,GAAG,CAAC,QAAkB;QACpB,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAClC,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,uCAAuC,CAAC,CAAC;QACtE,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,GAAa,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IACtF,CAAC;IAED;;;;;;;;;OASG;IACH,IAAI,CAAC,QAAkB,EAAE,WAAmB;QAC1C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,CAAC,CAAC;QACpC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;QAEvC,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,8DAA8D,CAAC,CAAC;QAC7F,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,yDAAyD,CAAC,CAAC;QAExF,OAAO,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,GAAa,EAAE,EAAE;YAC3C,IAAI,OAAO,GAAG,CAAC,CAAC;YAChB,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;gBACrB,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBACb,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,OAAO,CAAC;YACzC,CAAC;YACD,OAAO,OAAO,CAAC;QACjB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IACf,CAAC;IAED,KAAK;QACH,OAAQ,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,kCAAkC,CAAC,CAAC,GAAG,EAAoB,CAAC,CAAC,CAAC;IACxF,CAAC;IAED,aAAa,CAAC,IAAY;QACxB,OAAO,CACJ,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,4DAA4D,CAAC,CAAC,GAAG,CAAC,IAAI,CAEzE,IAAI,IAAI,CACvB,CAAC;IACJ,CAAC;IAED,aAAa,CAAC,IAAY,EAAE,KAAa,EAAE,MAAc,EAAE,IAAY;QACrE,IAAI,CAAC,EAAE;aACJ,OAAO,CACN;gHACwG,CACzG;aACA,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;IACpC,CAAC;IAED,OAAO,CAAC,GAAW;QACjB,OAAO,CACJ,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,sCAAsC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAmC,EAAE,KAAK;YAC1G,IAAI,CACL,CAAC;IACJ,CAAC;IAED,OAAO,CAAC,GAAW,EAAE,KAAa;QAChC,IAAI,CAAC,EAAE;aACJ,OAAO,CAAC,mGAAmG,CAAC;aAC5G,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACrB,CAAC;IAED,6CAA6C;IAC7C,SAAS;QACP,IAAI,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;QACpC,IAAI,CAAC,EAAE,EAAE,CAAC;YACR,EAAE,GAAG,UAAU,EAAE,CAAC;YAClB,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;QACjC,CAAC;QACD,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,KAAK;QACH,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;IAClB,CAAC;CACF"}
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":"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"}
@@ -1,2 +1,14 @@
1
- /** First meaningful line of a prompt, trimmed to a usable title length. */
2
- export declare function deriveTitle(text: string, maxLength?: number): string;
1
+ /**
2
+ * First meaningful line of a prompt, secret-redacted and NOT truncated.
3
+ *
4
+ * Truncation deliberately does not happen here. Cutting the line first slices a
5
+ * straddling secret in half, neither half matches its pattern any more, and the
6
+ * fragment ships inside the title. Only the built-in rules are reachable from an
7
+ * adapter, so doing the cut here could only ever be safe for those — an
8
+ * org-supplied pattern would still be bisected.
9
+ *
10
+ * The privacy pipeline redacts this field again with the org's own rules and
11
+ * only then truncates it (see TITLE_MAX_LENGTH in privacy/pipeline.ts), so the
12
+ * cut always lands on already-redacted text whatever the rule's source.
13
+ */
14
+ export declare function deriveTitle(text: string): string;