@faithfulalabi/agent-lens 0.1.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.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/bin/agent-lens.js +40 -0
- package/bin/package.json +4 -0
- package/dist/src/archive/cron-log.js +41 -0
- package/dist/src/archive/discover.js +89 -0
- package/dist/src/archive/index.js +7 -0
- package/dist/src/archive/lock.js +119 -0
- package/dist/src/archive/log.js +15 -0
- package/dist/src/archive/mirror.js +366 -0
- package/dist/src/archive/paths.js +159 -0
- package/dist/src/archive/read.js +133 -0
- package/dist/src/archive/report.js +272 -0
- package/dist/src/archive/seal.js +76 -0
- package/dist/src/archive/sidecar.js +39 -0
- package/dist/src/cli/args.js +47 -0
- package/dist/src/cli/commands/archive.js +65 -0
- package/dist/src/cli/commands/doctor.js +191 -0
- package/dist/src/cli/commands/prune.js +159 -0
- package/dist/src/cli/commands/rebuild.js +98 -0
- package/dist/src/cli/commands/schedule.js +325 -0
- package/dist/src/cli/commands/start.js +83 -0
- package/dist/src/cli/commands/warm.js +96 -0
- package/dist/src/cli/index.js +102 -0
- package/dist/src/content/resolve.js +163 -0
- package/dist/src/corpus/env.js +32 -0
- package/dist/src/corpus/paths.js +70 -0
- package/dist/src/corpus/scan.js +85 -0
- package/dist/src/corpus/watch.js +189 -0
- package/dist/src/db/freshness.js +82 -0
- package/dist/src/db/open.js +74 -0
- package/dist/src/db/read.js +266 -0
- package/dist/src/db/schema.js +312 -0
- package/dist/src/db/sidecars.js +216 -0
- package/dist/src/db/spill-index.js +68 -0
- package/dist/src/db/write.js +279 -0
- package/dist/src/project/pipeline.js +307 -0
- package/dist/src/project/subagents.js +41 -0
- package/dist/src/project/tools.js +94 -0
- package/dist/src/server/api.js +249 -0
- package/dist/src/server/app.js +28 -0
- package/dist/src/server/config.js +24 -0
- package/dist/src/server/drift-report.js +35 -0
- package/dist/src/server/index.js +1 -0
- package/dist/src/server/live.js +109 -0
- package/dist/src/server/middleware/host-guard.js +42 -0
- package/dist/src/server/middleware/token-auth.js +19 -0
- package/dist/src/server/start.js +150 -0
- package/dist/src/server/static-ui.js +97 -0
- package/dist/src/server/stream.js +50 -0
- package/dist/src/server/warm.js +48 -0
- package/dist/src/shared/api.js +1 -0
- package/dist/src/shared/entities.js +1 -0
- package/dist/src/shared/index.js +2 -0
- package/dist/src/shared/pricing.js +68 -0
- package/dist/src/shared/token.js +39 -0
- package/dist/src/transcript/accessors.js +28 -0
- package/dist/src/transcript/agents.js +44 -0
- package/dist/src/transcript/blocks.js +75 -0
- package/dist/src/transcript/drift.js +42 -0
- package/dist/src/transcript/human.js +65 -0
- package/dist/src/transcript/line.js +251 -0
- package/dist/src/transcript/raw-types.js +1 -0
- package/dist/src/transcript/spill.js +122 -0
- package/dist/src/transcript/usage.js +63 -0
- package/dist/src/transcript/version.js +1 -0
- package/package.json +70 -0
- package/ui/dist/assets/index-CKKoKUCq.js +254 -0
- package/ui/dist/assets/index-Chza4fL6.css +1 -0
- package/ui/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/ui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/ui/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/ui/dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/ui/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/ui/dist/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/ui/dist/index.html +21 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { closeSync, constants, fchmodSync, fstatSync, openSync, readSync, writeSync, } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { discover } from './discover.js';
|
|
5
|
+
import { acquireLock } from './lock.js';
|
|
6
|
+
import { appendArchiveLog } from './log.js';
|
|
7
|
+
import { sealArchiveFile } from './seal.js';
|
|
8
|
+
import { assertNotUnderRoot, canonicalizeTranscriptPath, ensureDir, ensureDirUnder, realpathDeepest, refuseSymlinkedLeaf, resolveArchiveLogPath, resolveArchiveRoot, resolveDataDir, resolveTranscriptRoot, statSafe, statSafeBig, TRANSCRIPT_ROOT_LABEL, } from './paths.js';
|
|
9
|
+
const { O_CREAT, O_EXCL, O_NOFOLLOW, O_RDONLY, O_RDWR, O_WRONLY } = constants;
|
|
10
|
+
const PROBE_BYTES = 4096;
|
|
11
|
+
const CHUNK_BYTES = 1024 * 1024;
|
|
12
|
+
const NEWLINE = 0x0a;
|
|
13
|
+
export function decideCopyEnd(input) {
|
|
14
|
+
const { archiveSize, sourceSize, lastNewlineOffset, settled } = input;
|
|
15
|
+
if (sourceSize <= archiveSize)
|
|
16
|
+
return archiveSize;
|
|
17
|
+
if (settled)
|
|
18
|
+
return sourceSize;
|
|
19
|
+
return lastNewlineOffset === undefined ? archiveSize : lastNewlineOffset + 1;
|
|
20
|
+
}
|
|
21
|
+
export function shouldSeal(input) {
|
|
22
|
+
const { sourceState, alreadySealed, archiveSize } = input;
|
|
23
|
+
return sourceState === 'expired' && !alreadySealed && archiveSize > 0;
|
|
24
|
+
}
|
|
25
|
+
function readInto(fd, buf, position, length) {
|
|
26
|
+
let read = 0;
|
|
27
|
+
while (read < length) {
|
|
28
|
+
const n = readSync(fd, buf, read, length - read, position + read);
|
|
29
|
+
if (n === 0)
|
|
30
|
+
break;
|
|
31
|
+
read += n;
|
|
32
|
+
}
|
|
33
|
+
return read;
|
|
34
|
+
}
|
|
35
|
+
export function readRange(fd, position, length) {
|
|
36
|
+
if (length <= 0)
|
|
37
|
+
return Buffer.alloc(0);
|
|
38
|
+
const buf = Buffer.allocUnsafe(length);
|
|
39
|
+
const read = readInto(fd, buf, position, length);
|
|
40
|
+
return read === length ? buf : buf.subarray(0, read);
|
|
41
|
+
}
|
|
42
|
+
function sha256(buf) {
|
|
43
|
+
return createHash('sha256').update(buf).digest('hex');
|
|
44
|
+
}
|
|
45
|
+
function hashPrefix(fd, length, buf) {
|
|
46
|
+
const hash = createHash('sha256');
|
|
47
|
+
let pos = 0;
|
|
48
|
+
let bytesRead = 0;
|
|
49
|
+
while (pos < length) {
|
|
50
|
+
const want = Math.min(buf.length, length - pos);
|
|
51
|
+
const got = readInto(fd, buf, pos, want);
|
|
52
|
+
if (got === 0)
|
|
53
|
+
break;
|
|
54
|
+
hash.update(buf.subarray(0, got));
|
|
55
|
+
pos += got;
|
|
56
|
+
bytesRead += got;
|
|
57
|
+
}
|
|
58
|
+
return { hash: hash.digest('hex'), bytesRead };
|
|
59
|
+
}
|
|
60
|
+
function scanDeltaBackward(fd, from, to, buf) {
|
|
61
|
+
let lastNewlineOffset;
|
|
62
|
+
let bytesRead = 0;
|
|
63
|
+
let end = to;
|
|
64
|
+
while (end > from) {
|
|
65
|
+
const start = Math.max(from, end - buf.length);
|
|
66
|
+
const want = end - start;
|
|
67
|
+
const got = readInto(fd, buf, start, want);
|
|
68
|
+
if (got === 0)
|
|
69
|
+
break;
|
|
70
|
+
bytesRead += got;
|
|
71
|
+
if (lastNewlineOffset === undefined) {
|
|
72
|
+
const idx = buf.subarray(0, got).lastIndexOf(NEWLINE);
|
|
73
|
+
if (idx !== -1)
|
|
74
|
+
lastNewlineOffset = start + idx;
|
|
75
|
+
}
|
|
76
|
+
end = start;
|
|
77
|
+
}
|
|
78
|
+
return { lastNewlineOffset, bytesRead };
|
|
79
|
+
}
|
|
80
|
+
function writeArchiveBytes(params) {
|
|
81
|
+
const { archiveRoot, archivePath, archiveExists, sourceFd, from, to, buffer } = params;
|
|
82
|
+
const dir = dirname(archivePath);
|
|
83
|
+
ensureDirUnder(dir, archiveRoot);
|
|
84
|
+
const fd = refuseSymlinkedLeaf(archivePath, () => openSync(archivePath, archiveExists ? O_RDWR | O_NOFOLLOW : O_WRONLY | O_CREAT | O_EXCL, 0o600));
|
|
85
|
+
try {
|
|
86
|
+
if (!archiveExists)
|
|
87
|
+
fchmodSync(fd, 0o600);
|
|
88
|
+
let pos = from;
|
|
89
|
+
let bytesRead = 0;
|
|
90
|
+
while (pos < to) {
|
|
91
|
+
const want = Math.min(buffer.length, to - pos);
|
|
92
|
+
const got = readInto(sourceFd, buffer, pos, want);
|
|
93
|
+
if (got === 0)
|
|
94
|
+
break;
|
|
95
|
+
bytesRead += got;
|
|
96
|
+
const size = fstatSync(fd).size;
|
|
97
|
+
if (size !== pos) {
|
|
98
|
+
throw new Error(`Invariant W violated for ${archivePath}: EOF is ${size} but the next write is at ${pos}`);
|
|
99
|
+
}
|
|
100
|
+
writeSync(fd, buffer, 0, got, pos);
|
|
101
|
+
pos += got;
|
|
102
|
+
}
|
|
103
|
+
return { bytesWritten: pos - from, bytesRead };
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
closeSync(fd);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export function detectDivergence(params) {
|
|
110
|
+
const { sourceFd, archiveFd, sourceSize, archiveSize, verify, buffer } = params;
|
|
111
|
+
if (archiveSize === 0)
|
|
112
|
+
return { reason: undefined, bytesRead: 0 };
|
|
113
|
+
if (sourceSize < archiveSize)
|
|
114
|
+
return { reason: 'shrink', bytesRead: 0 };
|
|
115
|
+
let bytesRead = 0;
|
|
116
|
+
const w = Math.min(PROBE_BYTES, archiveSize);
|
|
117
|
+
const sourceHead = readRange(sourceFd, 0, w);
|
|
118
|
+
bytesRead += sourceHead.length;
|
|
119
|
+
const archiveHead = readRange(archiveFd, 0, w);
|
|
120
|
+
if (!sourceHead.equals(archiveHead))
|
|
121
|
+
return { reason: 'head', bytesRead };
|
|
122
|
+
if (archiveSize > PROBE_BYTES) {
|
|
123
|
+
const seamStart = archiveSize - PROBE_BYTES;
|
|
124
|
+
const sourceSeam = readRange(sourceFd, seamStart, PROBE_BYTES);
|
|
125
|
+
bytesRead += sourceSeam.length;
|
|
126
|
+
const archiveSeam = readRange(archiveFd, seamStart, PROBE_BYTES);
|
|
127
|
+
if (!sourceSeam.equals(archiveSeam))
|
|
128
|
+
return { reason: 'seam', bytesRead };
|
|
129
|
+
}
|
|
130
|
+
if (verify) {
|
|
131
|
+
const sourcePrefix = hashPrefix(sourceFd, archiveSize, buffer);
|
|
132
|
+
bytesRead += sourcePrefix.bytesRead;
|
|
133
|
+
const archivePrefix = hashPrefix(archiveFd, archiveSize, buffer);
|
|
134
|
+
if (sourcePrefix.hash !== archivePrefix.hash)
|
|
135
|
+
return { reason: 'verify', bytesRead };
|
|
136
|
+
}
|
|
137
|
+
return { reason: undefined, bytesRead };
|
|
138
|
+
}
|
|
139
|
+
export function createMirrorContext(params) {
|
|
140
|
+
return {
|
|
141
|
+
archiveRoot: params.archiveRoot,
|
|
142
|
+
verify: params.verify === true,
|
|
143
|
+
buffer: Buffer.allocUnsafe(CHUNK_BYTES),
|
|
144
|
+
statFile: params.statFile ?? statSafeBig,
|
|
145
|
+
bytesRead: 0,
|
|
146
|
+
sealed: [],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function expire(entry, base, ctx) {
|
|
150
|
+
const expired = { ...base, source_state: 'expired' };
|
|
151
|
+
if (!shouldSeal({
|
|
152
|
+
sourceState: 'expired',
|
|
153
|
+
alreadySealed: entry.sealed,
|
|
154
|
+
archiveSize: base.archive_size,
|
|
155
|
+
})) {
|
|
156
|
+
return expired;
|
|
157
|
+
}
|
|
158
|
+
const sealed = sealArchiveFile(base.archive_path, ctx.archiveRoot);
|
|
159
|
+
ctx.sealed.push(base.archive_path);
|
|
160
|
+
return {
|
|
161
|
+
...expired,
|
|
162
|
+
archive_size: sealed.sealed_size,
|
|
163
|
+
archive_state: 'sealed',
|
|
164
|
+
archive_sha256: sealed.archive_sha256,
|
|
165
|
+
sealed_at: sealed.sealed_at,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
export function mirrorFile(entry, ctx) {
|
|
169
|
+
const archiveDiskPath = entry.sealed ? `${entry.archivePath}.zst` : entry.archivePath;
|
|
170
|
+
const archiveStat = statSafe(archiveDiskPath);
|
|
171
|
+
const archiveSize = archiveStat?.size ?? 0;
|
|
172
|
+
const archiveState = entry.sealed ? 'sealed' : 'hot';
|
|
173
|
+
const base = {
|
|
174
|
+
source_path: entry.sourcePath,
|
|
175
|
+
source_size: null,
|
|
176
|
+
source_mtime_ms: null,
|
|
177
|
+
source_head_sha256: null,
|
|
178
|
+
source_head_len: 0,
|
|
179
|
+
source_state: 'present',
|
|
180
|
+
archive_path: entry.archivePath,
|
|
181
|
+
archive_size: archiveSize,
|
|
182
|
+
archive_state: archiveState,
|
|
183
|
+
bytes_copied: 0,
|
|
184
|
+
};
|
|
185
|
+
const s0 = ctx.statFile(entry.sourcePath);
|
|
186
|
+
if (s0 === undefined || !s0.isFile()) {
|
|
187
|
+
return expire(entry, base, ctx);
|
|
188
|
+
}
|
|
189
|
+
const sourceSize = Number(s0.size);
|
|
190
|
+
base.source_size = sourceSize;
|
|
191
|
+
base.source_mtime_ms = Number(s0.mtimeMs);
|
|
192
|
+
let sourceFd;
|
|
193
|
+
try {
|
|
194
|
+
sourceFd = openSync(entry.sourcePath, 'r');
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (error.code === 'ENOENT') {
|
|
198
|
+
return expire(entry, base, ctx);
|
|
199
|
+
}
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
const headLen = Math.min(PROBE_BYTES, sourceSize);
|
|
204
|
+
const head = readRange(sourceFd, 0, headLen);
|
|
205
|
+
base.source_head_len = head.length;
|
|
206
|
+
base.source_head_sha256 = sha256(head);
|
|
207
|
+
ctx.bytesRead += head.length;
|
|
208
|
+
if (entry.sealed)
|
|
209
|
+
return base;
|
|
210
|
+
if (archiveSize > 0 && archiveStat !== undefined) {
|
|
211
|
+
const archiveFd = refuseSymlinkedLeaf(archiveDiskPath, () => openSync(archiveDiskPath, O_RDONLY | O_NOFOLLOW));
|
|
212
|
+
let reason;
|
|
213
|
+
try {
|
|
214
|
+
const detected = detectDivergence({
|
|
215
|
+
sourceFd,
|
|
216
|
+
archiveFd,
|
|
217
|
+
sourceSize,
|
|
218
|
+
archiveSize,
|
|
219
|
+
verify: ctx.verify,
|
|
220
|
+
buffer: ctx.buffer,
|
|
221
|
+
});
|
|
222
|
+
reason = detected.reason;
|
|
223
|
+
ctx.bytesRead += detected.bytesRead;
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
closeSync(archiveFd);
|
|
227
|
+
}
|
|
228
|
+
if (reason !== undefined)
|
|
229
|
+
return { ...base, source_state: 'diverged', reason };
|
|
230
|
+
}
|
|
231
|
+
if (sourceSize <= archiveSize)
|
|
232
|
+
return base;
|
|
233
|
+
const scan = scanDeltaBackward(sourceFd, archiveSize, sourceSize, ctx.buffer);
|
|
234
|
+
ctx.bytesRead += scan.bytesRead;
|
|
235
|
+
const s1 = ctx.statFile(entry.sourcePath);
|
|
236
|
+
const settled = s1 !== undefined && s1.size === s0.size && s1.mtimeNs === s0.mtimeNs;
|
|
237
|
+
const end = decideCopyEnd({
|
|
238
|
+
archiveSize,
|
|
239
|
+
sourceSize,
|
|
240
|
+
lastNewlineOffset: scan.lastNewlineOffset,
|
|
241
|
+
settled,
|
|
242
|
+
});
|
|
243
|
+
if (end <= archiveSize)
|
|
244
|
+
return base;
|
|
245
|
+
const written = writeArchiveBytes({
|
|
246
|
+
archiveRoot: ctx.archiveRoot,
|
|
247
|
+
archivePath: entry.archivePath,
|
|
248
|
+
archiveExists: archiveStat !== undefined,
|
|
249
|
+
sourceFd,
|
|
250
|
+
from: archiveSize,
|
|
251
|
+
to: end,
|
|
252
|
+
buffer: ctx.buffer,
|
|
253
|
+
});
|
|
254
|
+
ctx.bytesRead += written.bytesRead;
|
|
255
|
+
return {
|
|
256
|
+
...base,
|
|
257
|
+
archive_size: archiveSize + written.bytesWritten,
|
|
258
|
+
bytes_copied: written.bytesWritten,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
closeSync(sourceFd);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function ensureContainedArchiveRoot(dataDir, sourceRoot) {
|
|
266
|
+
const resolvedDataDir = realpathDeepest(dataDir);
|
|
267
|
+
assertNotUnderRoot(resolvedDataDir, sourceRoot, TRANSCRIPT_ROOT_LABEL);
|
|
268
|
+
const root = resolveArchiveRoot(resolvedDataDir);
|
|
269
|
+
assertNotUnderRoot(root, sourceRoot, TRANSCRIPT_ROOT_LABEL);
|
|
270
|
+
ensureDir(root);
|
|
271
|
+
assertNotUnderRoot(root, sourceRoot, TRANSCRIPT_ROOT_LABEL);
|
|
272
|
+
return root;
|
|
273
|
+
}
|
|
274
|
+
export function archiveOnce(options = {}) {
|
|
275
|
+
const sourceRoot = canonicalizeTranscriptPath(resolveTranscriptRoot(options.transcriptRoot));
|
|
276
|
+
const dataDir = resolveDataDir(options.dataDir);
|
|
277
|
+
let archiveRoot;
|
|
278
|
+
let lock;
|
|
279
|
+
try {
|
|
280
|
+
archiveRoot = canonicalizeTranscriptPath(ensureContainedArchiveRoot(dataDir, sourceRoot));
|
|
281
|
+
lock = acquireLock(dataDir, options.lockIdentity);
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
return {
|
|
285
|
+
files: [],
|
|
286
|
+
filesSeen: 0,
|
|
287
|
+
bytesCopied: 0,
|
|
288
|
+
bytesRead: 0,
|
|
289
|
+
lock: { state: 'not-attempted' },
|
|
290
|
+
errors: [{ path: dataDir, message: String(error), origin: 'archive' }],
|
|
291
|
+
sourceRoot,
|
|
292
|
+
archiveRoot: resolveArchiveRoot(dataDir),
|
|
293
|
+
logged: false,
|
|
294
|
+
sealed: [],
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const result = {
|
|
298
|
+
files: [],
|
|
299
|
+
filesSeen: 0,
|
|
300
|
+
bytesCopied: 0,
|
|
301
|
+
bytesRead: 0,
|
|
302
|
+
lock: lock.state,
|
|
303
|
+
errors: [],
|
|
304
|
+
sourceRoot,
|
|
305
|
+
archiveRoot,
|
|
306
|
+
logged: false,
|
|
307
|
+
sealed: [],
|
|
308
|
+
};
|
|
309
|
+
const diverged = [];
|
|
310
|
+
const newlyExpired = [];
|
|
311
|
+
try {
|
|
312
|
+
if (lock.state.state !== 'held') {
|
|
313
|
+
const entries = discover(sourceRoot, archiveRoot);
|
|
314
|
+
result.filesSeen = entries.length;
|
|
315
|
+
const ctx = createMirrorContext({ archiveRoot, verify: options.verify });
|
|
316
|
+
for (const entry of entries) {
|
|
317
|
+
try {
|
|
318
|
+
const state = mirrorFile(entry, ctx);
|
|
319
|
+
result.files.push(state);
|
|
320
|
+
result.bytesCopied += state.bytes_copied;
|
|
321
|
+
if (state.source_state === 'diverged') {
|
|
322
|
+
diverged.push({
|
|
323
|
+
source_path: state.source_path,
|
|
324
|
+
reason: state.reason ?? 'shrink',
|
|
325
|
+
archive_size: state.archive_size,
|
|
326
|
+
source_size: state.source_size,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
if (state.source_state === 'expired' && entry.presence !== 'archive-only') {
|
|
330
|
+
newlyExpired.push(state.source_path);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
const origin = error.path === entry.sourcePath ? 'source' : 'archive';
|
|
335
|
+
result.errors.push({ path: entry.sourcePath, message: String(error), origin });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
result.bytesRead = ctx.bytesRead;
|
|
339
|
+
result.sealed = ctx.sealed;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
finally {
|
|
343
|
+
lock.release();
|
|
344
|
+
}
|
|
345
|
+
const record = {
|
|
346
|
+
ts: new Date().toISOString(),
|
|
347
|
+
files_seen: result.filesSeen,
|
|
348
|
+
bytes_copied: result.bytesCopied,
|
|
349
|
+
lock: result.lock,
|
|
350
|
+
diverged,
|
|
351
|
+
newly_expired: newlyExpired,
|
|
352
|
+
errors: result.errors,
|
|
353
|
+
sealed: result.sealed,
|
|
354
|
+
};
|
|
355
|
+
try {
|
|
356
|
+
result.logged = appendArchiveLog(dataDir, record);
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
result.errors.push({
|
|
360
|
+
path: resolveArchiveLogPath(dataDir),
|
|
361
|
+
message: String(error),
|
|
362
|
+
origin: 'log',
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { closeSync, constants, fchmodSync, lstatSync, mkdirSync, openSync, realpathSync, statSync, writeSync, } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
const { O_APPEND, O_CREAT, O_NOFOLLOW, O_WRONLY } = constants;
|
|
5
|
+
const ARCHIVE_DIR = 'archive';
|
|
6
|
+
const LOGS_DIR = 'logs';
|
|
7
|
+
export function resolveDataDir(dir) {
|
|
8
|
+
return dir ?? process.env.AGENT_LENS_DIR ?? join(homedir(), '.agent-lens');
|
|
9
|
+
}
|
|
10
|
+
export function resolveTranscriptRoot(root) {
|
|
11
|
+
return root ?? process.env.AGENT_LENS_TRANSCRIPT_ROOT ?? join(homedir(), '.claude', 'projects');
|
|
12
|
+
}
|
|
13
|
+
export function canonicalizeTranscriptPath(path) {
|
|
14
|
+
try {
|
|
15
|
+
return realpathSync.native(path);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return resolve(path);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function realpathDeepest(path) {
|
|
22
|
+
const absolute = resolve(path);
|
|
23
|
+
const missing = [];
|
|
24
|
+
let current = absolute;
|
|
25
|
+
for (;;) {
|
|
26
|
+
try {
|
|
27
|
+
return join(realpathSync.native(current), ...missing);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
const parent = dirname(current);
|
|
31
|
+
if (parent === current)
|
|
32
|
+
return absolute;
|
|
33
|
+
missing.unshift(basename(current));
|
|
34
|
+
current = parent;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function resolveArchiveRoot(dataDir) {
|
|
39
|
+
return join(resolveDataDir(dataDir), ARCHIVE_DIR);
|
|
40
|
+
}
|
|
41
|
+
export function resolveArchiveLogPath(dataDir) {
|
|
42
|
+
return join(resolveDataDir(dataDir), LOGS_DIR, 'archive.jsonl');
|
|
43
|
+
}
|
|
44
|
+
export function resolveLockPath(dataDir) {
|
|
45
|
+
return join(resolveDataDir(dataDir), 'archive.lock');
|
|
46
|
+
}
|
|
47
|
+
export function resolveCronLogPath(dataDir) {
|
|
48
|
+
return join(resolveDataDir(dataDir), LOGS_DIR, 'cron.log');
|
|
49
|
+
}
|
|
50
|
+
export function resolveLaunchAgentsDir(homeDir = homedir()) {
|
|
51
|
+
return join(homeDir, 'Library', 'LaunchAgents');
|
|
52
|
+
}
|
|
53
|
+
export function resolvePlistPath(label, homeDir = homedir()) {
|
|
54
|
+
return join(resolveLaunchAgentsDir(homeDir), `${label}.plist`);
|
|
55
|
+
}
|
|
56
|
+
export function resolveScheduleWrapperPath(dataDir) {
|
|
57
|
+
return join(resolveDataDir(dataDir), 'schedule', 'archive.sh');
|
|
58
|
+
}
|
|
59
|
+
function isUnder(path, root) {
|
|
60
|
+
return path === root || path.startsWith(root.endsWith(sep) ? root : root + sep);
|
|
61
|
+
}
|
|
62
|
+
const ARCHIVE_ROOT_LABEL = 'the archive root';
|
|
63
|
+
export const DATA_DIR_LABEL = 'the data dir';
|
|
64
|
+
export const TRANSCRIPT_ROOT_LABEL = 'the transcript root';
|
|
65
|
+
export function assertUnderRoot(path, root, label) {
|
|
66
|
+
const resolved = realpathDeepest(path);
|
|
67
|
+
if (!isUnder(resolved, realpathDeepest(root))) {
|
|
68
|
+
throw new Error(`refusing to write outside ${label}: ${resolved}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function assertNotUnderRoot(path, root, label) {
|
|
72
|
+
const resolved = realpathDeepest(path);
|
|
73
|
+
if (isUnder(resolved, realpathDeepest(root))) {
|
|
74
|
+
throw new Error(`refusing to write inside ${label}: ${resolved}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function assertUnderArchiveRoot(path, archiveRoot) {
|
|
78
|
+
assertUnderRoot(path, archiveRoot, ARCHIVE_ROOT_LABEL);
|
|
79
|
+
}
|
|
80
|
+
export function isUnderAnyRoot(path, roots) {
|
|
81
|
+
if (roots.length === 0)
|
|
82
|
+
return false;
|
|
83
|
+
const resolvedRoots = roots.map((root) => realpathDeepest(root));
|
|
84
|
+
const contained = (candidate) => {
|
|
85
|
+
const resolved = realpathDeepest(candidate);
|
|
86
|
+
return resolvedRoots.some((root) => isUnder(resolved, root));
|
|
87
|
+
};
|
|
88
|
+
if (!contained(path))
|
|
89
|
+
return false;
|
|
90
|
+
return statSafe(path) !== undefined || contained(`${path}.zst`);
|
|
91
|
+
}
|
|
92
|
+
export function relativeUnder(root, path) {
|
|
93
|
+
const rel = relative(root, path);
|
|
94
|
+
if (rel === '' || isAbsolute(rel) || rel === '..' || rel.startsWith(`..${sep}`)) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
return rel;
|
|
98
|
+
}
|
|
99
|
+
export function ensureDir(dir) {
|
|
100
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
101
|
+
}
|
|
102
|
+
export function ensureDirUnder(dir, root, label = ARCHIVE_ROOT_LABEL) {
|
|
103
|
+
assertUnderRoot(dir, root, label);
|
|
104
|
+
ensureDir(dir);
|
|
105
|
+
assertUnderRoot(dir, root, label);
|
|
106
|
+
}
|
|
107
|
+
export function lstatSafe(path) {
|
|
108
|
+
try {
|
|
109
|
+
return lstatSync(path);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export function refuseSymlinkedLeaf(archivePath, open) {
|
|
116
|
+
try {
|
|
117
|
+
return open();
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
const code = error.code;
|
|
121
|
+
if (code === 'ELOOP') {
|
|
122
|
+
throw new Error(`refusing to follow a symlinked archive destination: ${archivePath} (ELOOP)`);
|
|
123
|
+
}
|
|
124
|
+
if (code === 'EEXIST') {
|
|
125
|
+
if (lstatSafe(archivePath)?.isSymbolicLink() === true) {
|
|
126
|
+
throw new Error(`refusing to follow a symlinked archive destination: ${archivePath} (EEXIST)`);
|
|
127
|
+
}
|
|
128
|
+
throw new Error(`archive destination already exists: ${archivePath} (EEXIST)`);
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export function appendOwnedLine(path, line, dataDir) {
|
|
134
|
+
ensureDirUnder(dirname(path), dataDir, DATA_DIR_LABEL);
|
|
135
|
+
const fd = refuseSymlinkedLeaf(path, () => openSync(path, O_WRONLY | O_CREAT | O_APPEND | O_NOFOLLOW, 0o600));
|
|
136
|
+
try {
|
|
137
|
+
fchmodSync(fd, 0o600);
|
|
138
|
+
writeSync(fd, line);
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
closeSync(fd);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export function statSafe(path) {
|
|
145
|
+
try {
|
|
146
|
+
return statSync(path);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export function statSafeBig(path) {
|
|
153
|
+
try {
|
|
154
|
+
return statSync(path, { bigint: true });
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { closeSync, fstatSync, openSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { zstdDecompressSync } from 'node:zlib';
|
|
3
|
+
import { readRange } from './mirror.js';
|
|
4
|
+
const SEALED_SUFFIX = '.zst';
|
|
5
|
+
const ZSTD_MAGIC = 0xfd2fb528;
|
|
6
|
+
const DEFAULT_MAX_ENTRIES = 4;
|
|
7
|
+
export const DEFAULT_MAX_BYTES = 64 * 1024 * 1024;
|
|
8
|
+
export function declaredContentSize(frame, label) {
|
|
9
|
+
if (frame.length < 5 || frame.readUInt32LE(0) !== ZSTD_MAGIC) {
|
|
10
|
+
throw new Error(`not a zstd frame: ${label}`);
|
|
11
|
+
}
|
|
12
|
+
const descriptor = frame[4];
|
|
13
|
+
const contentSizeFlag = descriptor >> 6;
|
|
14
|
+
const singleSegment = (descriptor >> 5) & 1;
|
|
15
|
+
const dictionaryIdFlag = descriptor & 0b11;
|
|
16
|
+
const offset = 5 + (singleSegment === 1 ? 0 : 1) + [0, 1, 2, 4][dictionaryIdFlag];
|
|
17
|
+
const width = contentSizeFlag === 0 ? (singleSegment === 1 ? 1 : 0) : [0, 2, 4, 8][contentSizeFlag];
|
|
18
|
+
if (width === 0) {
|
|
19
|
+
throw new Error(`sealed archive declares no content size: ${label}`);
|
|
20
|
+
}
|
|
21
|
+
if (offset + width > frame.length) {
|
|
22
|
+
throw new Error(`truncated zstd frame header: ${label}`);
|
|
23
|
+
}
|
|
24
|
+
switch (width) {
|
|
25
|
+
case 1:
|
|
26
|
+
return frame.readUInt8(offset);
|
|
27
|
+
case 2:
|
|
28
|
+
return frame.readUInt16LE(offset) + 256;
|
|
29
|
+
case 4:
|
|
30
|
+
return frame.readUInt32LE(offset);
|
|
31
|
+
default:
|
|
32
|
+
return Number(frame.readBigUInt64LE(offset));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function decompressSealedFrame(frame, declared, label, maxBytes) {
|
|
36
|
+
let out;
|
|
37
|
+
try {
|
|
38
|
+
out = zstdDecompressSync(frame, { maxOutputLength: maxBytes });
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error.code !== 'Z_BUF_ERROR')
|
|
42
|
+
throw error;
|
|
43
|
+
throw new Error(`truncated sealed archive ${label}: declares ${declared} bytes but 0 decompressed`);
|
|
44
|
+
}
|
|
45
|
+
if (out.length !== declared) {
|
|
46
|
+
throw new Error(`truncated sealed archive ${label}: declares ${declared} bytes but ${out.length} decompressed`);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
export function createArchiveReader(options = {}) {
|
|
51
|
+
const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
52
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
53
|
+
const cache = new Map();
|
|
54
|
+
let hits = 0;
|
|
55
|
+
let misses = 0;
|
|
56
|
+
let evictions = 0;
|
|
57
|
+
let cachedBytes = 0;
|
|
58
|
+
const openLogical = (archivePath) => {
|
|
59
|
+
try {
|
|
60
|
+
return { fd: openSync(archivePath, 'r'), sealed: false };
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error.code !== 'ENOENT')
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
return { fd: openSync(`${archivePath}${SEALED_SUFFIX}`, 'r'), sealed: true };
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error.code !== 'ENOENT')
|
|
71
|
+
throw error;
|
|
72
|
+
throw new Error(`no archived bytes for ${archivePath}`);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const loadSealed = (fd, archivePath) => {
|
|
76
|
+
const stat = fstatSync(fd, { bigint: true });
|
|
77
|
+
const key = `${archivePath}|${stat.ino}:${stat.mtimeNs}:${stat.size}`;
|
|
78
|
+
const cached = cache.get(key);
|
|
79
|
+
if (cached !== undefined) {
|
|
80
|
+
hits += 1;
|
|
81
|
+
cache.delete(key);
|
|
82
|
+
cache.set(key, cached);
|
|
83
|
+
return cached;
|
|
84
|
+
}
|
|
85
|
+
misses += 1;
|
|
86
|
+
const label = `${archivePath}${SEALED_SUFFIX}`;
|
|
87
|
+
const frame = readFileSync(fd);
|
|
88
|
+
const declared = declaredContentSize(frame, label);
|
|
89
|
+
if (declared > maxBytes) {
|
|
90
|
+
throw new Error(`sealed archive ${label} declares ${declared} bytes, over the ${maxBytes}-byte bound`);
|
|
91
|
+
}
|
|
92
|
+
const buf = decompressSealedFrame(frame, declared, label, maxBytes);
|
|
93
|
+
cache.set(key, buf);
|
|
94
|
+
cachedBytes += buf.length;
|
|
95
|
+
while (cache.size > maxEntries || cachedBytes > maxBytes) {
|
|
96
|
+
const oldest = cache.keys().next();
|
|
97
|
+
if (oldest.done === true)
|
|
98
|
+
break;
|
|
99
|
+
cachedBytes -= cache.get(oldest.value).length;
|
|
100
|
+
cache.delete(oldest.value);
|
|
101
|
+
evictions += 1;
|
|
102
|
+
}
|
|
103
|
+
return buf;
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
read(archivePath, offset, length) {
|
|
107
|
+
const { fd, sealed } = openLogical(archivePath);
|
|
108
|
+
try {
|
|
109
|
+
if (!sealed)
|
|
110
|
+
return readRange(fd, offset, length);
|
|
111
|
+
if (length <= 0)
|
|
112
|
+
return Buffer.alloc(0);
|
|
113
|
+
const buf = loadSealed(fd, archivePath);
|
|
114
|
+
return Buffer.from(buf.subarray(offset, Math.min(offset + length, buf.length)));
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
closeSync(fd);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
size(archivePath) {
|
|
121
|
+
const { fd, sealed } = openLogical(archivePath);
|
|
122
|
+
try {
|
|
123
|
+
return sealed ? loadSealed(fd, archivePath).length : fstatSync(fd).size;
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
closeSync(fd);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
stats() {
|
|
130
|
+
return { hits, misses, evictions, entries: cache.size, bytes: cachedBytes };
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|