@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.
- package/CHANGELOG.md +209 -1
- package/README.md +114 -38
- package/dist/adapters/claude.d.ts +18 -0
- package/dist/adapters/claude.js +153 -45
- package/dist/adapters/claude.js.map +1 -1
- package/dist/adapters/codex.d.ts +15 -1
- package/dist/adapters/codex.js +87 -34
- package/dist/adapters/codex.js.map +1 -1
- package/dist/adapters/opencode.d.ts +17 -6
- package/dist/adapters/opencode.js +72 -26
- package/dist/adapters/opencode.js.map +1 -1
- package/dist/adapters/types.d.ts +16 -0
- package/dist/adapters/types.js +61 -0
- package/dist/adapters/types.js.map +1 -1
- package/dist/cli.js +164 -33
- package/dist/cli.js.map +1 -1
- package/dist/commands/service.js +40 -10
- package/dist/commands/service.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +18 -5
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +73 -21
- package/dist/daemon.js +350 -119
- package/dist/daemon.js.map +1 -1
- package/dist/git/commits.d.ts +7 -1
- package/dist/git/commits.js +36 -17
- package/dist/git/commits.js.map +1 -1
- package/dist/git/repo.d.ts +13 -4
- package/dist/git/repo.js +34 -20
- package/dist/git/repo.js.map +1 -1
- package/dist/machine.d.ts +27 -0
- package/dist/machine.js +46 -0
- package/dist/machine.js.map +1 -0
- package/dist/privacy/pipeline.d.ts +6 -0
- package/dist/privacy/pipeline.js +41 -7
- package/dist/privacy/pipeline.js.map +1 -1
- package/dist/privacy/redact.d.ts +15 -2
- package/dist/privacy/redact.js +45 -6
- package/dist/privacy/redact.js.map +1 -1
- package/dist/queue/event-id.d.ts +9 -0
- package/dist/queue/event-id.js +15 -0
- package/dist/queue/event-id.js.map +1 -0
- package/dist/queue/spool.d.ts +24 -5
- package/dist/queue/spool.js +89 -33
- package/dist/queue/spool.js.map +1 -1
- package/dist/queue/tailer.d.ts +27 -4
- package/dist/queue/tailer.js +89 -28
- package/dist/queue/tailer.js.map +1 -1
- package/dist/sessions/title.d.ts +14 -2
- package/dist/sessions/title.js +18 -6
- package/dist/sessions/title.js.map +1 -1
- package/dist/transport/client.d.ts +37 -13
- package/dist/transport/client.js +50 -3
- package/dist/transport/client.js.map +1 -1
- package/package.json +2 -2
package/dist/privacy/redact.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
/**
|
|
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":"
|
|
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"}
|
package/dist/queue/spool.d.ts
CHANGED
|
@@ -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;
|
package/dist/queue/spool.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
124
|
+
return this.transaction(() => {
|
|
64
125
|
let written = 0;
|
|
65
|
-
for (const event of
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
})
|
|
171
|
+
});
|
|
117
172
|
}
|
|
118
173
|
depth() {
|
|
119
|
-
return this.
|
|
174
|
+
return this.stmt.count.get().n;
|
|
120
175
|
}
|
|
121
176
|
getCheckpoint(path) {
|
|
122
|
-
return
|
|
177
|
+
return this.checkpoints.get(path) ?? null;
|
|
123
178
|
}
|
|
124
179
|
setCheckpoint(path, inode, offset, size) {
|
|
125
|
-
this.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
132
|
-
null);
|
|
187
|
+
return this.stmt.getMeta.get(key)?.value ?? null;
|
|
133
188
|
}
|
|
134
189
|
setMeta(key, value) {
|
|
135
|
-
this.
|
|
136
|
-
|
|
137
|
-
|
|
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() {
|
package/dist/queue/spool.js.map
CHANGED
|
@@ -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;
|
|
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;AASzC;;;;;;;;;;GAUG;AACH,MAAM,OAAO,KAAK;IACC,EAAE,CAAoB;IACtB,IAAI,CAWnB;IACF;;;;OAIG;IACc,WAAW,GAAG,IAAI,GAAG,EAAsB,CAAC;IAE7D,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,qDAAqD;QACrD,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;QACrC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAC;QACvC,0EAA0E;QAC1E,2BAA2B;QAC3B,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,8BAA8B,CAAC,CAAC;QAC/C,gEAAgE;QAChE,iEAAiE;QACjE,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,uCAAuC;YACzC,CAAC;QACH,CAAC;QACD,IAAI,CAAC,OAAO,EAAE,CAAC;QACf,IAAI,CAAC,eAAe,EAAE,CAAC;QAEvB,IAAI,CAAC,IAAI,GAAG;YACV,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,4EAA4E,CAAC;YACrG,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,8EAA8E,CAAC;YACrG,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,uCAAuC,CAAC;YAC7D,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,8DAA8D,CAAC;YACrF,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,yDAAyD,CAAC;YAChF,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,kCAAkC,CAAC;YAC1D,UAAU,EAAE,IAAI,CAAC,EAAE,CAAC,OAAO,CACzB;gHACwG,CACzG;YACD,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,sCAAsC,CAAC;YAChE,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,OAAO,CACtB,mGAAmG,CACpG;YACD,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,gCAAgC,CAAC;SAC3D,CAAC;QAEF,IAAI,CAAC,eAAe,EAAE,CAAC;IACzB,CAAC;IAEO,eAAe;QACrB,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,mDAAmD,CAAC,CAAC,GAAG,EAEjF,CAAC;QACL,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;QACzB,KAAK,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,IAAI,IAAI;YAAE,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACrE,CAAC;IAEO,OAAO;QACb,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;KAoBZ,CAAC,CAAC;IACL,CAAC;IAED;;;;;OAKG;IACK,eAAe;QACrB,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,gBAAgB,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QACxE,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QACrE,IAAI,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,KAAK,GAAG,CAAC,EAAE,CAAC;YACpC,IAAI,CAAC;gBACH,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACzB,CAAC;YAAC,MAAM,CAAC;gBACP,uEAAuE;YACzE,CAAC;QACH,CAAC;IACH,CAAC;IAED,6EAA6E;IAC7E,WAAW,CAAI,EAAW;QACxB,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,CAAC;QACnC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,qEAAqE;YACrE,wEAAwE;YACxE,IAAI,CAAC,eAAe,EAAE,CAAC;YACvB,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,OAAO,CAAC,MAAuB;QAC7B,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,CAAC,CAAC;QAClC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE;YAC3B,IAAI,OAAO,GAAG,CAAC,CAAC;YAChB,KAAK,MAAM,KAAK,IAAI,MAAM;gBAAE,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC;YAChH,OAAO,OAAO,CAAC;QACjB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,iEAAiE;IACjE,IAAI,CAAC,KAAa;QAChB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAyC,CAAC;QAE/E,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,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC1E,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,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE;YAC3B,IAAI,OAAO,GAAG,CAAC,CAAC;YAChB,KAAK,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;gBAC1B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBACvB,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,OAAO,CAAC;YACnD,CAAC;YACD,OAAO,OAAO,CAAC;QACjB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK;QACH,OAAQ,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAoB,CAAC,CAAC,CAAC;IACpD,CAAC;IAED,aAAa,CAAC,IAAY;QACxB,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;IAC5C,CAAC;IAED,aAAa,CAAC,IAAY,EAAE,KAAa,EAAE,MAAc,EAAE,IAAY;QACrE,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC3C,IAAI,OAAO,IAAI,OAAO,CAAC,KAAK,KAAK,KAAK,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,IAAI,OAAO,CAAC,IAAI,KAAK,IAAI;YAAE,OAAO;QACrG,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;QACpD,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IACtD,CAAC;IAED,OAAO,CAAC,GAAW;QACjB,OAAQ,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAmC,EAAE,KAAK,IAAI,IAAI,CAAC;IACtF,CAAC;IAED,OAAO,CAAC,GAAW,EAAE,KAAa;QAChC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACpC,CAAC;IAED,UAAU,CAAC,GAAW;QACpB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,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"}
|
package/dist/queue/tailer.d.ts
CHANGED
|
@@ -2,8 +2,11 @@ import type { Spool } from './spool.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Resumable line tailer.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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:
|
|
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>;
|
package/dist/queue/tailer.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
2
|
import { open } from 'node:fs/promises';
|
|
3
3
|
const NEWLINE = 0x0a;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/dist/queue/tailer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tailer.js","sourceRoot":"","sources":["../../src/queue/tailer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"tailer.js","sourceRoot":"","sources":["../../src/queue/tailer.ts"],"names":[],"mappings":"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"}
|
package/dist/sessions/title.d.ts
CHANGED
|
@@ -1,2 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
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;
|