@agent-finops/core 0.6.0 → 0.6.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.
@@ -0,0 +1,54 @@
1
+ import { type ActivitySnapshot, type ActivitySnapshotRefreshErrorCode } from "./activitySnapshot.js";
2
+ export declare const activitySnapshotCacheEnvironmentVariable = "AIBILL_CACHE_DIR";
3
+ export declare const activitySnapshotCacheFileName = "statusline-v1.json";
4
+ export declare const activitySnapshotCacheMaxBytes: number;
5
+ export type ActivitySnapshotCacheOptions = {
6
+ /** Test/embedding override. Production defaults to ~/.aibill/cache. */
7
+ cacheDirectory?: string;
8
+ /** Test-only home override for validating the default private path. */
9
+ homeDirectory?: string;
10
+ /** Bounded wait for a concurrent writer. */
11
+ lockTimeoutMs?: number;
12
+ };
13
+ export type ActivitySnapshotCacheReadErrorCode = "unsafe_directory" | "unsafe_file" | "oversized" | "malformed" | "unsupported_version" | "permission" | "io";
14
+ export type ActivitySnapshotCacheReadResult = {
15
+ status: "ok";
16
+ snapshot: ActivitySnapshot;
17
+ } | {
18
+ status: "missing";
19
+ } | {
20
+ status: "error";
21
+ code: ActivitySnapshotCacheReadErrorCode;
22
+ };
23
+ export type ActivitySnapshotCacheWriteResult = {
24
+ status: "written";
25
+ snapshot: ActivitySnapshot;
26
+ } | {
27
+ status: "skipped_older";
28
+ snapshot: ActivitySnapshot;
29
+ };
30
+ export declare class ActivitySnapshotCacheError extends Error {
31
+ readonly code: ActivitySnapshotCacheReadErrorCode | "lock_timeout" | "invalid_snapshot";
32
+ constructor(code: ActivitySnapshotCacheError["code"], message: string);
33
+ }
34
+ /** Resolve the fixed cache filename without creating or trusting it. */
35
+ export declare function activitySnapshotCachePath(options?: ActivitySnapshotCacheOptions): string;
36
+ /**
37
+ * Read at most 64 KiB and validate the complete strict v1 contract. All
38
+ * expected unsafe/malformed states are returned as data so status surfaces can
39
+ * fail closed without printing local filesystem details.
40
+ */
41
+ export declare function readActivitySnapshot(options?: ActivitySnapshotCacheOptions): Promise<ActivitySnapshotCacheReadResult>;
42
+ /**
43
+ * Atomically publish a valid snapshot. Writers serialize through one bounded
44
+ * private lock; comparison happens while the lock is held so a late, older
45
+ * scan cannot replace newer evidence.
46
+ */
47
+ export declare function writeActivitySnapshot(snapshot: ActivitySnapshot, options?: ActivitySnapshotCacheOptions): Promise<ActivitySnapshotCacheWriteResult>;
48
+ /**
49
+ * Preserve the last-good financial values after a failed refresh. Only the
50
+ * typed attempt timestamp and sanitized error code change. With no prior good
51
+ * value, write a bounded error snapshot instead of manufacturing data.
52
+ */
53
+ export declare function recordActivitySnapshotRefreshFailure(attemptedAt: string, errorCode: ActivitySnapshotRefreshErrorCode, options?: ActivitySnapshotCacheOptions): Promise<ActivitySnapshotCacheWriteResult>;
54
+ //# sourceMappingURL=activitySnapshotCache.d.ts.map
@@ -0,0 +1,489 @@
1
+ import { constants } from "node:fs";
2
+ import { open, lstat, mkdir, chmod, realpath, rename, unlink } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { randomUUID } from "node:crypto";
6
+ import { setTimeout as delay } from "node:timers/promises";
7
+ import { activitySnapshotSchema, createActivitySnapshotError } from "./activitySnapshot.js";
8
+ export const activitySnapshotCacheEnvironmentVariable = "AIBILL_CACHE_DIR";
9
+ export const activitySnapshotCacheFileName = "statusline-v1.json";
10
+ export const activitySnapshotCacheMaxBytes = 64 * 1_024;
11
+ const lockFileName = ".statusline-v1.lock";
12
+ const defaultLockTimeoutMs = 2_000;
13
+ const staleLockMs = 15_000;
14
+ const lockPollMs = 20;
15
+ const lockMetadataMaxBytes = 512;
16
+ export class ActivitySnapshotCacheError extends Error {
17
+ code;
18
+ constructor(code, message) {
19
+ super(message);
20
+ this.name = "ActivitySnapshotCacheError";
21
+ this.code = code;
22
+ }
23
+ }
24
+ /** Resolve the fixed cache filename without creating or trusting it. */
25
+ export function activitySnapshotCachePath(options = {}) {
26
+ return join(configuredCacheDirectory(options), activitySnapshotCacheFileName);
27
+ }
28
+ /**
29
+ * Read at most 64 KiB and validate the complete strict v1 contract. All
30
+ * expected unsafe/malformed states are returned as data so status surfaces can
31
+ * fail closed without printing local filesystem details.
32
+ */
33
+ export async function readActivitySnapshot(options = {}) {
34
+ let directory;
35
+ try {
36
+ directory = await resolveCacheDirectory(false, options);
37
+ }
38
+ catch (error) {
39
+ if (isNodeError(error, "ENOENT"))
40
+ return { status: "missing" };
41
+ return { status: "error", code: cacheReadErrorCode(error, "unsafe_directory") };
42
+ }
43
+ return readSnapshotFile(directory);
44
+ }
45
+ /**
46
+ * Atomically publish a valid snapshot. Writers serialize through one bounded
47
+ * private lock; comparison happens while the lock is held so a late, older
48
+ * scan cannot replace newer evidence.
49
+ */
50
+ export async function writeActivitySnapshot(snapshot, options = {}) {
51
+ let candidate;
52
+ try {
53
+ candidate = activitySnapshotSchema.parse(snapshot);
54
+ }
55
+ catch {
56
+ throw new ActivitySnapshotCacheError("invalid_snapshot", "Activity snapshot does not match the strict v1 contract.");
57
+ }
58
+ return withWriterLock(options, async (directory) => {
59
+ const existing = await readSnapshotFile(directory);
60
+ if (existing.status === "ok" && !isNewer(candidate, existing.snapshot)) {
61
+ return { status: "skipped_older", snapshot: existing.snapshot };
62
+ }
63
+ if (existing.status === "error" &&
64
+ (existing.code === "unsafe_file" || existing.code === "permission" ||
65
+ existing.code === "unsupported_version")) {
66
+ throw new ActivitySnapshotCacheError(existing.code, "Refusing to replace an unsafe or inaccessible activity snapshot cache file.");
67
+ }
68
+ await atomicWriteSnapshot(directory, candidate);
69
+ return { status: "written", snapshot: candidate };
70
+ });
71
+ }
72
+ /**
73
+ * Preserve the last-good financial values after a failed refresh. Only the
74
+ * typed attempt timestamp and sanitized error code change. With no prior good
75
+ * value, write a bounded error snapshot instead of manufacturing data.
76
+ */
77
+ export async function recordActivitySnapshotRefreshFailure(attemptedAt, errorCode, options = {}) {
78
+ const parsedAttempt = new Date(attemptedAt);
79
+ if (!Number.isFinite(parsedAttempt.getTime())) {
80
+ throw new ActivitySnapshotCacheError("invalid_snapshot", "Refresh attempt time must be a valid ISO timestamp.");
81
+ }
82
+ const normalizedAttempt = parsedAttempt.toISOString();
83
+ return withWriterLock(options, async (directory) => {
84
+ const existing = await readSnapshotFile(directory);
85
+ if (existing.status === "ok") {
86
+ const newestExistingEvent = Math.max(Date.parse(existing.snapshot.lastAttemptAt), Date.parse(existing.snapshot.generatedAt));
87
+ if (Date.parse(normalizedAttempt) <= newestExistingEvent) {
88
+ return { status: "skipped_older", snapshot: existing.snapshot };
89
+ }
90
+ const failed = activitySnapshotSchema.parse({
91
+ ...existing.snapshot,
92
+ lastAttemptAt: normalizedAttempt,
93
+ refresh: { status: "error", errorCode }
94
+ });
95
+ await atomicWriteSnapshot(directory, failed);
96
+ return { status: "written", snapshot: failed };
97
+ }
98
+ if (existing.status === "error" &&
99
+ (existing.code === "unsafe_file" || existing.code === "permission" ||
100
+ existing.code === "unsupported_version")) {
101
+ throw new ActivitySnapshotCacheError(existing.code, "Refusing to replace an unsafe or inaccessible activity snapshot cache file.");
102
+ }
103
+ const failed = createActivitySnapshotError(normalizedAttempt, errorCode);
104
+ await atomicWriteSnapshot(directory, failed);
105
+ return { status: "written", snapshot: failed };
106
+ });
107
+ }
108
+ async function withWriterLock(options, operation) {
109
+ const directory = await resolveCacheDirectory(true, options);
110
+ const lockPath = join(directory, lockFileName);
111
+ const timeout = boundedLockTimeout(options.lockTimeoutMs);
112
+ const started = Date.now();
113
+ let lockHandle;
114
+ let lockIdentity;
115
+ while (!lockHandle) {
116
+ let candidateHandle;
117
+ let candidateIdentity;
118
+ try {
119
+ const owner = { pid: process.pid, token: randomUUID() };
120
+ candidateHandle = await open(lockPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollowFlag(), 0o600);
121
+ const candidateInfo = await candidateHandle.stat();
122
+ candidateIdentity = { ...owner, dev: candidateInfo.dev, ino: candidateInfo.ino };
123
+ await candidateHandle.writeFile(`${JSON.stringify(owner)}\n`, "utf8");
124
+ await candidateHandle.sync();
125
+ lockHandle = candidateHandle;
126
+ lockIdentity = candidateIdentity;
127
+ candidateHandle = undefined;
128
+ candidateIdentity = undefined;
129
+ }
130
+ catch (error) {
131
+ await candidateHandle?.close().catch(() => undefined);
132
+ if (candidateIdentity)
133
+ await releaseOwnedLock(lockPath, candidateIdentity);
134
+ if (!isNodeError(error, "EEXIST")) {
135
+ if (isNodeError(error, "ELOOP")) {
136
+ throw new ActivitySnapshotCacheError("unsafe_file", "Activity snapshot writer lock is a symbolic link.");
137
+ }
138
+ throw error;
139
+ }
140
+ await removeStaleLock(lockPath);
141
+ if (Date.now() - started >= timeout) {
142
+ throw new ActivitySnapshotCacheError("lock_timeout", "Timed out waiting for the activity snapshot writer lock.");
143
+ }
144
+ await delay(lockPollMs);
145
+ }
146
+ }
147
+ try {
148
+ return await operation(directory);
149
+ }
150
+ finally {
151
+ await lockHandle.close().catch(() => undefined);
152
+ if (lockIdentity)
153
+ await releaseOwnedLock(lockPath, lockIdentity);
154
+ }
155
+ }
156
+ async function readSnapshotFile(directory) {
157
+ const filePath = join(directory, activitySnapshotCacheFileName);
158
+ let fileInfo;
159
+ try {
160
+ fileInfo = await lstat(filePath);
161
+ }
162
+ catch (error) {
163
+ if (isNodeError(error, "ENOENT"))
164
+ return { status: "missing" };
165
+ return { status: "error", code: cacheReadErrorCode(error, "io") };
166
+ }
167
+ if (fileInfo.isSymbolicLink() || !fileInfo.isFile()) {
168
+ return { status: "error", code: "unsafe_file" };
169
+ }
170
+ if (!hasPrivatePermissions(fileInfo.mode)) {
171
+ return { status: "error", code: "unsafe_file" };
172
+ }
173
+ if (fileInfo.size > activitySnapshotCacheMaxBytes) {
174
+ return { status: "error", code: "oversized" };
175
+ }
176
+ let handle;
177
+ try {
178
+ handle = await open(filePath, constants.O_RDONLY | noFollowFlag());
179
+ const openedInfo = await handle.stat();
180
+ if (!openedInfo.isFile())
181
+ return { status: "error", code: "unsafe_file" };
182
+ if (!hasPrivatePermissions(openedInfo.mode)) {
183
+ return { status: "error", code: "unsafe_file" };
184
+ }
185
+ if (openedInfo.size > activitySnapshotCacheMaxBytes) {
186
+ return { status: "error", code: "oversized" };
187
+ }
188
+ // Bound the actual read, not only the preceding stat: the file may grow
189
+ // after inspection. One extra byte is enough to detect overflow without
190
+ // ever allocating or reading an attacker-controlled whole file.
191
+ const bounded = Buffer.allocUnsafe(activitySnapshotCacheMaxBytes + 1);
192
+ let bytesRead = 0;
193
+ while (bytesRead < bounded.length) {
194
+ const result = await handle.read(bounded, bytesRead, bounded.length - bytesRead, bytesRead);
195
+ if (result.bytesRead === 0)
196
+ break;
197
+ bytesRead += result.bytesRead;
198
+ }
199
+ if (bytesRead > activitySnapshotCacheMaxBytes) {
200
+ return { status: "error", code: "oversized" };
201
+ }
202
+ const contents = bounded.subarray(0, bytesRead).toString("utf8");
203
+ let value;
204
+ try {
205
+ value = JSON.parse(contents);
206
+ }
207
+ catch {
208
+ return { status: "error", code: "malformed" };
209
+ }
210
+ if (isRecord(value) && value.schemaVersion !== undefined && value.schemaVersion !== 1) {
211
+ return { status: "error", code: "unsupported_version" };
212
+ }
213
+ const parsed = activitySnapshotSchema.safeParse(value);
214
+ return parsed.success
215
+ ? { status: "ok", snapshot: parsed.data }
216
+ : { status: "error", code: "malformed" };
217
+ }
218
+ catch (error) {
219
+ if (isNodeError(error, "ELOOP"))
220
+ return { status: "error", code: "unsafe_file" };
221
+ return { status: "error", code: cacheReadErrorCode(error, "io") };
222
+ }
223
+ finally {
224
+ await handle?.close().catch(() => undefined);
225
+ }
226
+ }
227
+ async function atomicWriteSnapshot(directory, snapshot) {
228
+ const contents = `${JSON.stringify(snapshot)}\n`;
229
+ if (Buffer.byteLength(contents, "utf8") > activitySnapshotCacheMaxBytes) {
230
+ throw new ActivitySnapshotCacheError("invalid_snapshot", "Activity snapshot exceeds the 64 KiB cache limit.");
231
+ }
232
+ const filePath = join(directory, activitySnapshotCacheFileName);
233
+ const existing = await lstat(filePath).catch((error) => {
234
+ if (isNodeError(error, "ENOENT"))
235
+ return undefined;
236
+ throw error;
237
+ });
238
+ if (existing?.isSymbolicLink() || (existing && !existing.isFile())) {
239
+ throw new ActivitySnapshotCacheError("unsafe_file", "Activity snapshot cache path is not a regular file.");
240
+ }
241
+ const temporaryPath = join(directory, `.${activitySnapshotCacheFileName}.${process.pid}.${randomUUID()}.tmp`);
242
+ let handle;
243
+ try {
244
+ handle = await open(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollowFlag(), 0o600);
245
+ await handle.writeFile(contents, "utf8");
246
+ await handle.sync();
247
+ await handle.close();
248
+ handle = undefined;
249
+ await rename(temporaryPath, filePath);
250
+ await chmod(filePath, 0o600);
251
+ await syncDirectory(directory);
252
+ }
253
+ catch (error) {
254
+ await handle?.close().catch(() => undefined);
255
+ await unlink(temporaryPath).catch(() => undefined);
256
+ throw error;
257
+ }
258
+ }
259
+ async function resolveCacheDirectory(create, options) {
260
+ const usesDefaultDirectory = !options.cacheDirectory?.trim() &&
261
+ !process.env[activitySnapshotCacheEnvironmentVariable]?.trim();
262
+ if (usesDefaultDirectory) {
263
+ await ensureDefaultParent(options.homeDirectory ?? homedir(), create);
264
+ }
265
+ const requested = configuredCacheDirectory(options);
266
+ let info = await lstat(requested).catch((error) => {
267
+ if (isNodeError(error, "ENOENT"))
268
+ return undefined;
269
+ throw error;
270
+ });
271
+ if (!info && create) {
272
+ if (usesDefaultDirectory) {
273
+ await mkdir(requested, { mode: 0o700 }).catch((error) => {
274
+ if (!isNodeError(error, "EEXIST"))
275
+ throw error;
276
+ });
277
+ }
278
+ else {
279
+ await mkdir(requested, { recursive: true, mode: 0o700 });
280
+ }
281
+ info = await lstat(requested);
282
+ }
283
+ if (!info) {
284
+ const error = new Error("Activity snapshot cache directory does not exist.");
285
+ error.code = "ENOENT";
286
+ throw error;
287
+ }
288
+ if (info.isSymbolicLink() || !info.isDirectory()) {
289
+ throw new ActivitySnapshotCacheError("unsafe_directory", "Activity snapshot cache directory is not a real directory.");
290
+ }
291
+ if (!create && !hasPrivatePermissions(info.mode)) {
292
+ throw new ActivitySnapshotCacheError("unsafe_directory", "Activity snapshot cache directory is not private.");
293
+ }
294
+ const canonical = await realpath(requested);
295
+ const confirmed = await lstat(requested);
296
+ if (confirmed.isSymbolicLink() || !confirmed.isDirectory()) {
297
+ throw new ActivitySnapshotCacheError("unsafe_directory", "Activity snapshot cache directory changed during validation.");
298
+ }
299
+ if (create)
300
+ await chmod(canonical, 0o700);
301
+ return canonical;
302
+ }
303
+ async function ensureDefaultParent(homeDirectory, create) {
304
+ const parent = join(homeDirectory, ".aibill");
305
+ let info = await lstat(parent).catch((error) => {
306
+ if (isNodeError(error, "ENOENT"))
307
+ return undefined;
308
+ throw error;
309
+ });
310
+ if (!info && create) {
311
+ await mkdir(parent, { mode: 0o700 }).catch((error) => {
312
+ // Two first-ever writers may both observe ENOENT. The winner creates
313
+ // the directory; the loser must re-validate it rather than fail.
314
+ if (!isNodeError(error, "EEXIST"))
315
+ throw error;
316
+ });
317
+ info = await lstat(parent);
318
+ }
319
+ if (!info) {
320
+ const error = new Error("The private aibill directory does not exist.");
321
+ error.code = "ENOENT";
322
+ throw error;
323
+ }
324
+ if (info.isSymbolicLink() || !info.isDirectory()) {
325
+ throw new ActivitySnapshotCacheError("unsafe_directory", "The private aibill directory is not a real directory.");
326
+ }
327
+ if (!create && !hasPrivatePermissions(info.mode)) {
328
+ throw new ActivitySnapshotCacheError("unsafe_directory", "The private aibill directory is not private.");
329
+ }
330
+ if (create)
331
+ await chmod(parent, 0o700);
332
+ }
333
+ function configuredCacheDirectory(options) {
334
+ const configured = options.cacheDirectory?.trim() ||
335
+ process.env[activitySnapshotCacheEnvironmentVariable]?.trim();
336
+ const value = configured && configured.length > 0
337
+ ? configured
338
+ : join(options.homeDirectory ?? homedir(), ".aibill", "cache");
339
+ return resolve(value);
340
+ }
341
+ async function removeStaleLock(lockPath) {
342
+ let handle;
343
+ try {
344
+ handle = await open(lockPath, constants.O_RDONLY | noFollowFlag());
345
+ const info = await handle.stat();
346
+ if (!info.isFile() || !hasPrivatePermissions(info.mode)) {
347
+ throw new ActivitySnapshotCacheError("unsafe_file", "Activity snapshot writer lock is not a private regular file.");
348
+ }
349
+ if (Date.now() - info.mtimeMs <= staleLockMs)
350
+ return;
351
+ const owner = await readLockOwner(handle);
352
+ // Age alone never proves abandonment: a live writer may be paused while
353
+ // holding the lock. Unknown/malformed ownership also fails closed.
354
+ if (!owner || processIsAlive(owner.pid))
355
+ return;
356
+ await handle.close();
357
+ handle = undefined;
358
+ await releaseOwnedLock(lockPath, { ...owner, dev: info.dev, ino: info.ino });
359
+ }
360
+ catch (error) {
361
+ if (isNodeError(error, "ENOENT"))
362
+ return;
363
+ if (isNodeError(error, "ELOOP")) {
364
+ throw new ActivitySnapshotCacheError("unsafe_file", "Activity snapshot writer lock is a symbolic link.");
365
+ }
366
+ throw error;
367
+ }
368
+ finally {
369
+ await handle?.close().catch(() => undefined);
370
+ }
371
+ }
372
+ async function releaseOwnedLock(lockPath, identity) {
373
+ let handle;
374
+ try {
375
+ handle = await open(lockPath, constants.O_RDONLY | noFollowFlag());
376
+ const info = await handle.stat();
377
+ if (!info.isFile() || info.dev !== identity.dev || info.ino !== identity.ino)
378
+ return;
379
+ const owner = await readLockOwner(handle);
380
+ if (!owner || owner.pid !== identity.pid || owner.token !== identity.token)
381
+ return;
382
+ await handle.close();
383
+ handle = undefined;
384
+ const confirmed = await lstat(lockPath).catch((error) => {
385
+ if (isNodeError(error, "ENOENT"))
386
+ return undefined;
387
+ throw error;
388
+ });
389
+ if (!confirmed || confirmed.isSymbolicLink() ||
390
+ confirmed.dev !== identity.dev || confirmed.ino !== identity.ino) {
391
+ return;
392
+ }
393
+ // A live owner cannot be legitimately evicted, so once both inode and
394
+ // unguessable token match, unlinking cannot release another writer's lock.
395
+ await unlink(lockPath).catch((error) => {
396
+ if (!isNodeError(error, "ENOENT"))
397
+ throw error;
398
+ });
399
+ }
400
+ catch (error) {
401
+ if (!isNodeError(error, "ENOENT") && !isNodeError(error, "ELOOP"))
402
+ throw error;
403
+ }
404
+ finally {
405
+ await handle?.close().catch(() => undefined);
406
+ }
407
+ }
408
+ async function readLockOwner(handle) {
409
+ const buffer = Buffer.allocUnsafe(lockMetadataMaxBytes + 1);
410
+ const result = await handle.read(buffer, 0, buffer.length, 0);
411
+ if (result.bytesRead === 0 || result.bytesRead > lockMetadataMaxBytes)
412
+ return undefined;
413
+ let value;
414
+ try {
415
+ value = JSON.parse(buffer.subarray(0, result.bytesRead).toString("utf8"));
416
+ }
417
+ catch {
418
+ return undefined;
419
+ }
420
+ if (!isRecord(value) || !Number.isSafeInteger(value.pid) ||
421
+ value.pid <= 0 || typeof value.token !== "string" ||
422
+ value.token.length < 16 || value.token.length > 128) {
423
+ return undefined;
424
+ }
425
+ return { pid: value.pid, token: value.token };
426
+ }
427
+ function processIsAlive(pid) {
428
+ try {
429
+ process.kill(pid, 0);
430
+ return true;
431
+ }
432
+ catch (error) {
433
+ return !isNodeError(error, "ESRCH");
434
+ }
435
+ }
436
+ async function syncDirectory(directory) {
437
+ let handle;
438
+ try {
439
+ handle = await open(directory, constants.O_RDONLY);
440
+ await handle.sync().catch((error) => {
441
+ if (!isNodeError(error, "EINVAL") && !isNodeError(error, "ENOTSUP"))
442
+ throw error;
443
+ });
444
+ }
445
+ finally {
446
+ await handle?.close().catch(() => undefined);
447
+ }
448
+ }
449
+ function isNewer(candidate, existing) {
450
+ const attemptDifference = Date.parse(candidate.lastAttemptAt) - Date.parse(existing.lastAttemptAt);
451
+ if (attemptDifference !== 0)
452
+ return attemptDifference > 0;
453
+ const generatedDifference = Date.parse(candidate.generatedAt) - Date.parse(existing.generatedAt);
454
+ if (generatedDifference !== 0)
455
+ return generatedDifference > 0;
456
+ return false;
457
+ }
458
+ function boundedLockTimeout(value) {
459
+ if (value === undefined)
460
+ return defaultLockTimeoutMs;
461
+ if (!Number.isFinite(value))
462
+ return defaultLockTimeoutMs;
463
+ return Math.max(0, Math.min(10_000, Math.floor(value)));
464
+ }
465
+ function cacheReadErrorCode(error, fallback) {
466
+ if (error instanceof ActivitySnapshotCacheError &&
467
+ (error.code === "unsafe_directory" || error.code === "unsafe_file" ||
468
+ error.code === "oversized" || error.code === "malformed" ||
469
+ error.code === "unsupported_version" || error.code === "permission" ||
470
+ error.code === "io")) {
471
+ return error.code;
472
+ }
473
+ if (isNodeError(error, "EACCES") || isNodeError(error, "EPERM"))
474
+ return "permission";
475
+ return fallback;
476
+ }
477
+ function noFollowFlag() {
478
+ return typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
479
+ }
480
+ function hasPrivatePermissions(mode) {
481
+ return process.platform === "win32" || (mode & 0o077) === 0;
482
+ }
483
+ function isRecord(value) {
484
+ return typeof value === "object" && value !== null && !Array.isArray(value);
485
+ }
486
+ function isNodeError(error, code) {
487
+ return error instanceof Error && error.code === code;
488
+ }
489
+ //# sourceMappingURL=activitySnapshotCache.js.map
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from "./analyze.js";
2
2
  export * from "./agentInventory.js";
3
+ export * from "./activitySnapshot.js";
4
+ export * from "./activitySnapshotCache.js";
3
5
  export * from "./attribution.js";
4
6
  export * from "./credentialDetection.js";
5
7
  export * from "./contextHealth.js";
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from "./analyze.js";
2
2
  export * from "./agentInventory.js";
3
+ export * from "./activitySnapshot.js";
4
+ export * from "./activitySnapshotCache.js";
3
5
  export * from "./attribution.js";
4
6
  export * from "./credentialDetection.js";
5
7
  export * from "./contextHealth.js";
@@ -109,6 +109,12 @@ export type LocalAgentLogOptions = {
109
109
  /** Collect privacy-safe Codex invocation summaries during the same JSON pass. */
110
110
  collectCodexInvocationEvidence?: boolean;
111
111
  };
112
+ /**
113
+ * Options accepted by the financial-only loader. Invocation collection is
114
+ * intentionally unavailable: this path reads only the evidence needed for a
115
+ * financial snapshot and transcript-reported plan limits.
116
+ */
117
+ export type LocalAgentFinancialLogOptions = Omit<LocalAgentLogOptions, "collectCodexInvocationEvidence">;
112
118
  export type LocalAgentLogDiagnosticCode = "directory_missing" | "directory_unreadable" | "file_unreadable" | "malformed_jsonl" | "unsupported_token_shape";
113
119
  export type LocalAgentLogDiagnostic = {
114
120
  agent: LocalAgentCall["agent"];
@@ -123,9 +129,22 @@ export type LocalAgentSourceScan = {
123
129
  directoryStatus: "readable" | "missing" | "unreadable";
124
130
  filesDiscovered: number;
125
131
  filesParsed: number;
132
+ /** Malformed lines observed inside `jsonlValidationCoverage`. */
126
133
  malformedLines: number;
127
134
  unreadableFiles: number;
128
135
  unsupportedUsageSnapshots: number;
136
+ /** Regular files safely excluded because their metadata predates `sinceIso`. */
137
+ filesSkippedBeforeWindow?: number;
138
+ /** Codex files resolved from bounded head/tail financial evidence. */
139
+ filesReadFinancially?: number;
140
+ /** Bytes not replayed as events after the required Codex financial state was proved. */
141
+ bytesSkippedAsNonFinancialHistory?: number;
142
+ /** JSONL lines classified from their envelope and skipped before JSON decoding. */
143
+ nonFinancialLinesPrefiltered?: number;
144
+ /** Bytes covered by the non-financial event prefilter. */
145
+ nonFinancialBytesPrefiltered?: number;
146
+ /** Whether JSON syntax was checked for every line or financial events only. */
147
+ jsonlValidationCoverage?: "complete" | "financial_events_only";
129
148
  };
130
149
  export type LocalAgentLogResult = {
131
150
  records: UsageRecord[];
@@ -159,6 +178,17 @@ export declare function parseClaudeCodeTranscript(content: string, filePath?: st
159
178
  export declare function parseCodexRollout(content: string, onEntry?: (entry: Record<string, unknown>) => void, onDiagnostic?: TranscriptParseDiagnosticHandler): LocalAgentCall[];
160
179
  /** Scan this machine's agent logs and return aggregated UsageRecords. */
161
180
  export declare function loadLocalAgentUsage(options?: LocalAgentLogOptions): Promise<LocalAgentLogResult>;
181
+ /**
182
+ * Stream only the financial evidence required by init/status snapshots.
183
+ *
184
+ * Unlike `loadLocalAgentUsage`, this path never derives prompts, file focus,
185
+ * tool activity, or invocation evidence. It deliberately returns the same
186
+ * result contract so callers can preserve the existing evidence and
187
+ * diagnostics vocabulary without maintaining a second loader schema. Codex
188
+ * project attribution intentionally uses only root metadata; home-launched
189
+ * tool-workdir inference remains exclusive to the full qualitative loader.
190
+ */
191
+ export declare function loadLocalAgentFinancialUsage(options?: LocalAgentFinancialLogOptions): Promise<LocalAgentLogResult>;
162
192
  /** Aggregate per-call usage into one UsageRecord per day+agent+model+project. */
163
193
  export declare function aggregateCalls(calls: LocalAgentCall[]): UsageRecord[];
164
194
  /**