@yusukeshib/pi-babysit 0.6.26 → 0.6.27
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/README.md +12 -54
- package/index.ts +80 -702
- package/package.json +1 -3
- package/log-source.ts +0 -232
- package/log-viewer.ts +0 -496
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yusukeshib/pi-babysit",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.27",
|
|
4
4
|
"description": "Run any shell command and pi subagents under babysit, with context-safe captured output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -20,8 +20,6 @@
|
|
|
20
20
|
"type": "module",
|
|
21
21
|
"files": [
|
|
22
22
|
"index.ts",
|
|
23
|
-
"log-viewer.ts",
|
|
24
|
-
"log-source.ts",
|
|
25
23
|
"agents.ts",
|
|
26
24
|
"self-reap.ts",
|
|
27
25
|
"format-stream.mjs",
|
package/log-source.ts
DELETED
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
|
|
4
|
-
export interface LogPage {
|
|
5
|
-
start: number;
|
|
6
|
-
end: number;
|
|
7
|
-
text: string;
|
|
8
|
-
lineTimestamps: (number | undefined)[];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** Byte cursors, not an index: only the selected sliding window is retained. */
|
|
12
|
-
export class FileLogSource {
|
|
13
|
-
readonly pageBytes: number;
|
|
14
|
-
size = 0;
|
|
15
|
-
metadataVersion = "";
|
|
16
|
-
private identity = "";
|
|
17
|
-
private modified = 0;
|
|
18
|
-
constructor(readonly file: string, readonly isAgent = false, pageBytes = 64 * 1024) {
|
|
19
|
-
this.pageBytes = Math.max(1, pageBytes);
|
|
20
|
-
}
|
|
21
|
-
/** A changed identity or shrinking file invalidates every cursor. */
|
|
22
|
-
refresh(): boolean {
|
|
23
|
-
try {
|
|
24
|
-
const stat = fs.statSync(path.join(path.dirname(this.file), "output.timestamps.jsonl"));
|
|
25
|
-
this.metadataVersion = `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}`;
|
|
26
|
-
} catch { this.metadataVersion = ""; }
|
|
27
|
-
try {
|
|
28
|
-
const stat = fs.statSync(this.file);
|
|
29
|
-
const identity = `${stat.dev}:${stat.ino}`;
|
|
30
|
-
const reset = identity !== this.identity || stat.size < this.size ||
|
|
31
|
-
(stat.size === this.size && stat.mtimeMs !== this.modified);
|
|
32
|
-
this.identity = identity;
|
|
33
|
-
this.modified = stat.mtimeMs;
|
|
34
|
-
this.size = stat.size;
|
|
35
|
-
return reset;
|
|
36
|
-
} catch {
|
|
37
|
-
const reset = this.identity !== "";
|
|
38
|
-
this.identity = "";
|
|
39
|
-
this.size = 0;
|
|
40
|
-
return reset;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
private bytes(fd: number, start: number, end: number): Buffer {
|
|
44
|
-
const data = Buffer.alloc(Math.max(0, end - start));
|
|
45
|
-
let read = 0;
|
|
46
|
-
while (read < data.length) {
|
|
47
|
-
const n = fs.readSync(fd, data, read, data.length - read, start + read);
|
|
48
|
-
if (!n) break;
|
|
49
|
-
read += n;
|
|
50
|
-
}
|
|
51
|
-
return data.subarray(0, read);
|
|
52
|
-
}
|
|
53
|
-
/** Find record boundaries in fixed-size reads. Keep assistant snapshots and
|
|
54
|
-
* subsequent tool results together, but never require a whole agent task:
|
|
55
|
-
* each new assistant message is another safe page boundary. Limits are soft
|
|
56
|
-
* for a single response/result group or oversized individual record. */
|
|
57
|
-
private boundary(fd: number, position: number, direction: -1 | 1): number {
|
|
58
|
-
const chunk = 8192;
|
|
59
|
-
let cursor = Math.max(0, Math.min(position, this.size));
|
|
60
|
-
if (!this.isAgent) {
|
|
61
|
-
while (direction < 0 ? cursor > 0 : cursor < this.size) {
|
|
62
|
-
const start = direction < 0 ? Math.max(0, cursor - chunk) : cursor;
|
|
63
|
-
const end = direction < 0 ? cursor : Math.min(this.size, cursor + chunk);
|
|
64
|
-
const data = this.bytes(fd, start, Math.min(this.size, end + 1));
|
|
65
|
-
for (let i = direction < 0 ? end - start - 1 : 0;
|
|
66
|
-
direction < 0 ? i >= 0 : i < end - start; i += direction) {
|
|
67
|
-
if (data[i] !== 10 && data[i] !== 13) continue;
|
|
68
|
-
const boundary = start + i + (data[i] === 13 && data[i + 1] === 10 ? 2 : 1);
|
|
69
|
-
if (direction < 0 ? boundary <= position : boundary >= position) return boundary;
|
|
70
|
-
}
|
|
71
|
-
cursor = direction < 0 ? start : end;
|
|
72
|
-
}
|
|
73
|
-
return direction < 0 ? 0 : this.size;
|
|
74
|
-
}
|
|
75
|
-
// Scan records in non-overlapping chunks. Carry only the record crossing
|
|
76
|
-
// the chunk edge, so normal pages do not reread 8 KiB for every event.
|
|
77
|
-
cursor = this.recordBoundary(fd, position, direction < 0 ? 1 : -1);
|
|
78
|
-
let parts: Buffer[] = [];
|
|
79
|
-
let recordStart = cursor;
|
|
80
|
-
const isStart = (start: number, segments: Buffer[]) => {
|
|
81
|
-
if (!(direction < 0 ? start <= position : start >= position)) return false;
|
|
82
|
-
try {
|
|
83
|
-
const event = JSON.parse(Buffer.concat(segments).toString("utf8"));
|
|
84
|
-
return event?.type === "agent_start" ||
|
|
85
|
-
(event?.type === "message_start" && event.message?.role === "assistant");
|
|
86
|
-
} catch { return false; }
|
|
87
|
-
};
|
|
88
|
-
while (direction < 0 ? cursor > 0 : cursor < this.size) {
|
|
89
|
-
const start = direction < 0 ? Math.max(0, cursor - chunk) : cursor;
|
|
90
|
-
const end = direction < 0 ? cursor : Math.min(this.size, cursor + chunk);
|
|
91
|
-
const data = this.bytes(fd, start, Math.min(this.size, end + 1));
|
|
92
|
-
let edge = direction < 0 ? end - start : 0;
|
|
93
|
-
for (let i = direction < 0 ? end - start - 1 : 0;
|
|
94
|
-
direction < 0 ? i >= 0 : i < end - start; i += direction) {
|
|
95
|
-
if (data[i] !== 10 && data[i] !== 13) continue;
|
|
96
|
-
if (data[i] === 13 && data[i + 1] === 10) continue;
|
|
97
|
-
if (direction < 0) {
|
|
98
|
-
const segments = [data.subarray(i + 1, edge), ...parts];
|
|
99
|
-
if (isStart(start + i + 1, segments)) return start + i + 1;
|
|
100
|
-
parts = []; edge = i + 1;
|
|
101
|
-
} else {
|
|
102
|
-
parts.push(data.subarray(edge, i + 1));
|
|
103
|
-
if (isStart(recordStart, parts)) return recordStart;
|
|
104
|
-
parts = []; edge = i + 1; recordStart = start + edge;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
if (direction < 0) parts.unshift(data.subarray(0, edge));
|
|
108
|
-
else parts.push(data.subarray(edge, end - start));
|
|
109
|
-
cursor = direction < 0 ? start : end;
|
|
110
|
-
}
|
|
111
|
-
if (isStart(direction < 0 ? 0 : recordStart, parts)) return direction < 0 ? 0 : recordStart;
|
|
112
|
-
return direction < 0 ? 0 : this.size;
|
|
113
|
-
}
|
|
114
|
-
private recordBoundary(fd: number, position: number, direction: -1 | 1): number {
|
|
115
|
-
// Same byte scanner, without interpreting agent events.
|
|
116
|
-
const processSource = new FileLogSource(this.file, false, this.pageBytes);
|
|
117
|
-
processSource.size = this.size;
|
|
118
|
-
return processSource.boundary(fd, position, direction);
|
|
119
|
-
}
|
|
120
|
-
private empty(): LogPage { return { start: 0, end: 0, text: "", lineTimestamps: [] }; }
|
|
121
|
-
private page(fd: number, start: number, end: number): LogPage {
|
|
122
|
-
const data = this.bytes(fd, start, end);
|
|
123
|
-
return { start, end: start + data.length, text: data.toString("utf8"), lineTimestamps: this.timestamps(data, start) };
|
|
124
|
-
}
|
|
125
|
-
before(end = this.size): LogPage {
|
|
126
|
-
if (!this.size) return this.empty();
|
|
127
|
-
let fd: number | undefined;
|
|
128
|
-
try {
|
|
129
|
-
fd = fs.openSync(this.file, "r");
|
|
130
|
-
return this.page(fd, this.boundary(fd, Math.max(0, end - this.pageBytes), -1), end);
|
|
131
|
-
} catch { this.dispose(); return this.empty(); }
|
|
132
|
-
finally { if (fd !== undefined) fs.closeSync(fd); }
|
|
133
|
-
}
|
|
134
|
-
after(start: number): LogPage {
|
|
135
|
-
if (!this.size) return this.empty();
|
|
136
|
-
let fd: number | undefined;
|
|
137
|
-
try {
|
|
138
|
-
fd = fs.openSync(this.file, "r");
|
|
139
|
-
return this.page(fd, start, this.boundary(fd, Math.min(this.size, start + this.pageBytes), 1));
|
|
140
|
-
} catch { this.dispose(); return this.empty(); }
|
|
141
|
-
finally { if (fd !== undefined) fs.closeSync(fd); }
|
|
142
|
-
}
|
|
143
|
-
/** Sorted sidecar byte ranges permit a binary seek without a persistent index.
|
|
144
|
-
* Capture metadata is optional; malformed records have no trusted timestamp. */
|
|
145
|
-
private timestamps(data: Buffer, offset: number): (number | undefined)[] {
|
|
146
|
-
const times: (number | undefined)[] = [];
|
|
147
|
-
const positions = [offset];
|
|
148
|
-
for (let i = 0; i < data.length; i++) {
|
|
149
|
-
if (data[i] !== 10 && data[i] !== 13) continue;
|
|
150
|
-
if (data[i] === 13 && data[i + 1] === 10) i++;
|
|
151
|
-
positions.push(offset + i + 1);
|
|
152
|
-
}
|
|
153
|
-
let fd: number | undefined;
|
|
154
|
-
try {
|
|
155
|
-
fd = fs.openSync(path.join(path.dirname(this.file), "output.timestamps.jsonl"), "r");
|
|
156
|
-
const size = fs.fstatSync(fd).size;
|
|
157
|
-
const record = (at: number) => {
|
|
158
|
-
let start = at;
|
|
159
|
-
if (at > 0) {
|
|
160
|
-
while (start > 0) {
|
|
161
|
-
const begin = Math.max(0, start - 1024);
|
|
162
|
-
const b = this.bytes(fd!, begin, start);
|
|
163
|
-
const newline = b.lastIndexOf(10);
|
|
164
|
-
if (newline >= 0) { start = begin + newline + 1; break; }
|
|
165
|
-
start = begin;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
let end = start;
|
|
169
|
-
const chunks: Buffer[] = [];
|
|
170
|
-
while (end < size) {
|
|
171
|
-
const b = this.bytes(fd!, end, Math.min(size, end + 1024));
|
|
172
|
-
if (!b.length) break;
|
|
173
|
-
const newline = b.indexOf(10);
|
|
174
|
-
const n = newline < 0 ? b.length : newline + 1;
|
|
175
|
-
chunks.push(b.subarray(0, n)); end += n;
|
|
176
|
-
if (newline >= 0) break;
|
|
177
|
-
}
|
|
178
|
-
let value: number[] | undefined;
|
|
179
|
-
try {
|
|
180
|
-
const r = JSON.parse(Buffer.concat(chunks).toString());
|
|
181
|
-
if (Array.isArray(r) && r.length === 3 && r.every(Number.isSafeInteger) && r[0] >= 0 && r[1] > 0 && Number.isSafeInteger(r[0] + r[1]) && r[2] >= 0 && r[2] <= 8640000000000000) value = r;
|
|
182
|
-
} catch { /* Optional malformed metadata. */ }
|
|
183
|
-
return { start, end, value };
|
|
184
|
-
};
|
|
185
|
-
let low = 0, high = size;
|
|
186
|
-
while (low < high) {
|
|
187
|
-
const r = record(Math.floor((low + high) / 2));
|
|
188
|
-
// An invalid seek pivot gives no ordering evidence. Do not fall back
|
|
189
|
-
// to a full sidecar scan; metadata is optional, raw history is not.
|
|
190
|
-
if (!r.value) return positions.map(() => undefined);
|
|
191
|
-
if (r.value[0] + r.value[1] <= offset) low = Math.max(low + 1, r.end);
|
|
192
|
-
else high = r.start;
|
|
193
|
-
}
|
|
194
|
-
let position = 0, cursor = low;
|
|
195
|
-
let pending: Buffer[] = [];
|
|
196
|
-
let previousEnd = 0;
|
|
197
|
-
while (cursor < size && position < positions.length) {
|
|
198
|
-
let chunk = this.bytes(fd, cursor, Math.min(size, cursor + 8192));
|
|
199
|
-
if (!chunk.length) break;
|
|
200
|
-
cursor += chunk.length;
|
|
201
|
-
if (cursor === size && chunk.at(-1) !== 10) chunk = Buffer.concat([chunk, Buffer.from("\n")]);
|
|
202
|
-
let edge = 0;
|
|
203
|
-
for (let i = 0; i < chunk.length && position < positions.length; i++) {
|
|
204
|
-
if (chunk[i] !== 10) continue;
|
|
205
|
-
pending.push(chunk.subarray(edge, i)); edge = i + 1;
|
|
206
|
-
const text = Buffer.concat(pending).toString(); pending = [];
|
|
207
|
-
let value: number[];
|
|
208
|
-
try {
|
|
209
|
-
value = JSON.parse(text);
|
|
210
|
-
if (!Array.isArray(value) || value.length !== 3 || !value.every(Number.isSafeInteger) ||
|
|
211
|
-
value[0] < previousEnd || value[1] <= 0 || !Number.isSafeInteger(value[0] + value[1]) ||
|
|
212
|
-
value[2] < 0 || value[2] > 8640000000000000) continue;
|
|
213
|
-
} catch { continue; }
|
|
214
|
-
const [start, length, time] = value;
|
|
215
|
-
previousEnd = start + length;
|
|
216
|
-
while (position < positions.length && positions[position] < start) times[position++] = undefined;
|
|
217
|
-
while (position < positions.length && positions[position] < start + length) {
|
|
218
|
-
times[position] = positions[position] < offset + data.length ? time : undefined;
|
|
219
|
-
position++;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
pending.push(chunk.subarray(edge));
|
|
223
|
-
}
|
|
224
|
-
} catch { /* Missing sidecars are normal for old logs. */ }
|
|
225
|
-
finally { if (fd !== undefined) fs.closeSync(fd); }
|
|
226
|
-
return positions.map((_, i) => times[i]);
|
|
227
|
-
}
|
|
228
|
-
retime(page: LogPage): LogPage {
|
|
229
|
-
return { ...page, lineTimestamps: this.timestamps(Buffer.from(page.text), page.start) };
|
|
230
|
-
}
|
|
231
|
-
dispose() { this.identity = ""; this.size = 0; this.metadataVersion = ""; }
|
|
232
|
-
}
|