agentel 0.2.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/src/archive.js ADDED
@@ -0,0 +1,1130 @@
1
+ "use strict";
2
+
3
+ const crypto = require("crypto");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const { loadConfig } = require("./config");
7
+ const { normalizeSessionEvents } = require("./canonical-events");
8
+ const { parserVersionForSource } = require("./parser-versions");
9
+ const { canonicalRepo } = require("./repo");
10
+ const { loadRedactionConfig, loadEnvValues, mergeSummaries, redactText, styleRedactionMarkersForMarkdown } = require("./redaction");
11
+ const { ensureBaseDirs, ensureDir, paths, readJson, writeJson } = require("./paths");
12
+
13
+ const SHARED_RAW_SOURCE_CACHE = new Map();
14
+
15
+ function archiveRoot(env = process.env) {
16
+ const cfg = loadConfig(env);
17
+ return path.join(cfg.storage.root || paths(env).data, "agentlog");
18
+ }
19
+
20
+ function objectPathForSession(session, env = process.env) {
21
+ const started = new Date(session.startedAt || Date.now());
22
+ const year = String(started.getUTCFullYear()).padStart(4, "0");
23
+ const month = String(started.getUTCMonth() + 1).padStart(2, "0");
24
+ const day = String(started.getUTCDate()).padStart(2, "0");
25
+ const root = archiveRoot(env);
26
+ const scopeSegment = session.scopeCanonical
27
+ ? `scope=${encodeSegment(session.scopeCanonical)}`
28
+ : `repo=${encodeSegment(session.repoCanonical)}`;
29
+ return path.join(
30
+ root,
31
+ "sessions",
32
+ scopeSegment,
33
+ session.provider ? `provider=${session.provider}` : "",
34
+ `year=${year}`,
35
+ `month=${month}`,
36
+ `day=${day}`
37
+ );
38
+ }
39
+
40
+ function writeSession(input, env = process.env) {
41
+ ensureBaseDirs(paths(env));
42
+ const cfg = loadConfig(env);
43
+ const cwd = input.scopeCanonical ? input.cwd || "" : input.cwd || process.cwd();
44
+ const redactionCwd = cwd || process.cwd();
45
+ const repoInfo = input.scopeCanonical ? null : input.repoInfo || canonicalRepo(cwd);
46
+ const provider = input.provider || "unknown";
47
+ const sourceType = input.sourceType || "import";
48
+ const parserVersion = input.parserVersion ?? parserVersionForSource(sourceType);
49
+ const messages = normalizeMessages(input.messages || []).map((message) => ({
50
+ ...message,
51
+ metadata: {
52
+ ...(message.metadata || {}),
53
+ sourceType: message.metadata?.sourceType || sourceType,
54
+ parserVersion: message.metadata?.parserVersion ?? parserVersion ?? undefined
55
+ }
56
+ }));
57
+ const startedAt = input.startedAt || messages[0]?.timestamp || new Date().toISOString();
58
+ const endedAt = input.endedAt || messages[messages.length - 1]?.timestamp || startedAt;
59
+ const sessionId = input.sessionId || stableSessionId(provider, input.sourcePath || "", startedAt, messages);
60
+ const repoCanonical = input.scopeCanonical ? "" : input.repoCanonical || repoInfo.key;
61
+ const redactionConfig = loadRedactionConfig(env);
62
+ const envValues = loadEnvValues(redactionCwd, redactionConfig.envVars, env);
63
+ const redactionSummary = {};
64
+ const redactedMessages = messages.map((message) => {
65
+ const result = redactText(message.content, {
66
+ config: redactionConfig,
67
+ cwd: redactionCwd,
68
+ env,
69
+ envValues,
70
+ repoCanonical
71
+ });
72
+ mergeSummaries(redactionSummary, result.summary);
73
+ return {
74
+ ...message,
75
+ content: result.text,
76
+ metadata: redactStructuredStrings(message.metadata || {}, {
77
+ config: redactionConfig,
78
+ cwd: redactionCwd,
79
+ env,
80
+ envValues,
81
+ repoCanonical,
82
+ summary: redactionSummary
83
+ })
84
+ };
85
+ });
86
+
87
+ const session = {
88
+ version: 1,
89
+ archiveSchemaVersion: 2,
90
+ sessionId,
91
+ provider,
92
+ scopeCanonical: input.scopeCanonical || "",
93
+ repoCanonical,
94
+ repoSource: input.scopeCanonical ? "scope" : repoInfo.source,
95
+ cwd,
96
+ title: input.title || "",
97
+ sourcePath: input.sourcePath || "",
98
+ sourceType,
99
+ parserVersion,
100
+ parserVersions: input.parserVersions || (parserVersion ? { [sourceType]: parserVersion } : {}),
101
+ storageScope: input.storageScope || (input.scopeCanonical ? cfg.privacy.webChatsDefaultScope : "repo"),
102
+ startedAt,
103
+ endedAt,
104
+ messageCount: redactedMessages.length,
105
+ userMessageCount: redactedMessages.filter((message) => message.role === "user").length,
106
+ redactionSummary,
107
+ importedAt: new Date().toISOString()
108
+ };
109
+ for (const key of [
110
+ "providerConversationId",
111
+ "chatAccountId",
112
+ "chatUsername",
113
+ "chatDisplayName",
114
+ "chatProjectPath",
115
+ "chatVirtualRepo",
116
+ "chatDisplayPath",
117
+ "conversationKind",
118
+ "pinned"
119
+ ]) {
120
+ if (input[key] !== undefined) session[key] = input[key];
121
+ }
122
+
123
+ const dir = objectPathForSession(session, env);
124
+ ensureDir(dir);
125
+ const conversationPath = path.join(dir, `session=${sessionId}.conversation.md`);
126
+ const metadataPath = path.join(dir, `session=${sessionId}.metadata.json`);
127
+ const transcriptPath = path.join(dir, `session=${sessionId}.transcript.jsonl`);
128
+ const eventPath = path.join(dir, `session=${sessionId}.events.jsonl`);
129
+ const viewPath = path.join(dir, `session=${sessionId}.view.json`);
130
+ const rawPath = path.join(dir, `session=${sessionId}.raw`);
131
+ const events = redactCanonicalEvents(
132
+ Array.isArray(input.events) ? normalizePrecomputedEvents(input.events, session) : normalizeSessionEvents(session, redactedMessages),
133
+ {
134
+ config: redactionConfig,
135
+ cwd: redactionCwd,
136
+ env,
137
+ envValues,
138
+ repoCanonical,
139
+ summary: redactionSummary
140
+ }
141
+ );
142
+ session.eventCount = events.length;
143
+ removeStaleSessionCopies(session, metadataPath, env, { replaceSourcePathCopies: input.replaceSourcePathCopies !== false });
144
+ const rawFiles = copyRawFiles(input, rawPath, env);
145
+ if (rawFiles.length) {
146
+ session.rawPath = rawPath;
147
+ session.rawFiles = rawFiles;
148
+ session.rawFileCount = rawFiles.length;
149
+ }
150
+ fs.writeFileSync(conversationPath, renderConversationMarkdown(session, redactedMessages, events), { mode: 0o600 });
151
+ fs.writeFileSync(transcriptPath, redactedMessages.map((message) => `${JSON.stringify(message)}\n`).join(""), {
152
+ mode: 0o600
153
+ });
154
+ fs.writeFileSync(eventPath, events.map((event) => `${JSON.stringify(event)}\n`).join(""), { mode: 0o600 });
155
+ writeSessionView(viewPath, computeSessionView(session, redactedMessages, events));
156
+ session.conversationPath = conversationPath;
157
+ session.transcriptPath = transcriptPath;
158
+ session.eventPath = eventPath;
159
+ session.viewPath = viewPath;
160
+ session.viewSchemaVersion = VIEW_SCHEMA_VERSION;
161
+ const usageStats = computeSessionUsage(redactedMessages);
162
+ if (usageStats) session.usage = usageStats;
163
+ const models = collectSessionModels(redactedMessages);
164
+ if (models.length) session.models = models;
165
+ writeJson(metadataPath, session);
166
+
167
+ if (cfg.privacy.revealCache && !input.scopeCanonical) {
168
+ const revealDir = path.join(paths(env).revealCache, provider);
169
+ ensureDir(revealDir);
170
+ fs.writeFileSync(
171
+ path.join(revealDir, `${sessionId}.jsonl`),
172
+ messages.map((message) => `${JSON.stringify(message)}\n`).join(""),
173
+ { mode: 0o600 }
174
+ );
175
+ }
176
+
177
+ touchManifest(session, { conversationPath, metadataPath, transcriptPath, eventPath, viewPath, rawPath: session.rawPath || "" }, env);
178
+ return { session, conversationPath, metadataPath, transcriptPath, eventPath, viewPath, rawPath: session.rawPath || "" };
179
+ }
180
+
181
+ function removeStaleSessionCopies(session, metadataPath, env = process.env, options = {}) {
182
+ const root = path.join(archiveRoot(env), "sessions");
183
+ const replaceSourcePathCopies = options.replaceSourcePathCopies !== false;
184
+ walk(root, (file) => {
185
+ if (!file.endsWith(".metadata.json") || path.resolve(file) === path.resolve(metadataPath)) return;
186
+ const metadata = readJson(file, null);
187
+ if (!metadata || metadata.provider !== session.provider) return;
188
+ const sameSession = metadata.sessionId === session.sessionId;
189
+ const sameSource = replaceSourcePathCopies && session.sourcePath && metadata.sourcePath === session.sourcePath;
190
+ if (!sameSession && !sameSource) return;
191
+ safeUnlink(metadata.conversationPath || file.replace(/\.metadata\.json$/, ".conversation.md"));
192
+ safeUnlink(metadata.transcriptPath || file.replace(/\.metadata\.json$/, ".transcript.jsonl"));
193
+ safeUnlink(metadata.eventPath || file.replace(/\.metadata\.json$/, ".events.jsonl"));
194
+ safeUnlink(metadata.viewPath || file.replace(/\.metadata\.json$/, ".view.json"));
195
+ safeRm(metadata.rawPath || file.replace(/\.metadata\.json$/, ".raw"));
196
+ safeUnlink(file);
197
+ });
198
+ }
199
+
200
+ function deleteSessionArchive(session, env = process.env) {
201
+ if (!session || typeof session !== "object") return false;
202
+ const metadataPath = session.metadataPath || "";
203
+ safeUnlink(session.conversationPath || metadataPath.replace(/\.metadata\.json$/, ".conversation.md"));
204
+ safeUnlink(session.transcriptPath || metadataPath.replace(/\.metadata\.json$/, ".transcript.jsonl"));
205
+ safeUnlink(session.eventPath || metadataPath.replace(/\.metadata\.json$/, ".events.jsonl"));
206
+ safeUnlink(session.viewPath || metadataPath.replace(/\.metadata\.json$/, ".view.json"));
207
+ safeRm(session.rawPath || metadataPath.replace(/\.metadata\.json$/, ".raw"));
208
+ safeUnlink(metadataPath);
209
+ if (session.provider && session.sessionId) {
210
+ safeUnlink(path.join(paths(env).revealCache, session.provider, `${session.sessionId}.jsonl`));
211
+ }
212
+ return true;
213
+ }
214
+
215
+ function copyRawFiles(input, rawPath, env = process.env) {
216
+ const files = rawSourceFiles(input);
217
+ const references = rawReferences(input);
218
+ if (!files.length && !references.length) {
219
+ safeRm(rawPath);
220
+ return [];
221
+ }
222
+ safeRm(rawPath);
223
+ ensureDir(rawPath);
224
+ const records = [];
225
+ for (const reference of references) {
226
+ records.push(reference);
227
+ }
228
+ if (input.sharedRawFiles) {
229
+ for (const file of files) {
230
+ const record = copySharedRawFile(file, env);
231
+ if (record) records.push(record);
232
+ }
233
+ if (!records.length) {
234
+ safeRm(rawPath);
235
+ return [];
236
+ }
237
+ writeJson(path.join(rawPath, "manifest.json"), {
238
+ version: 1,
239
+ copiedAt: new Date().toISOString(),
240
+ files: records
241
+ });
242
+ return records;
243
+ }
244
+ for (const file of files) {
245
+ const stat = safeStat(file);
246
+ if (!stat || !stat.isFile()) continue;
247
+ const filename = `${String(records.length + 1).padStart(3, "0")}-${safeRawFilename(path.basename(file))}`;
248
+ const archivedPath = path.join(rawPath, filename);
249
+ fs.copyFileSync(file, archivedPath);
250
+ fs.chmodSync(archivedPath, 0o600);
251
+ records.push({
252
+ originalPath: file,
253
+ filename,
254
+ archivedPath,
255
+ size: stat.size,
256
+ mtime: new Date(stat.mtimeMs).toISOString(),
257
+ sha256: fileSha256(archivedPath)
258
+ });
259
+ }
260
+ if (!records.length) {
261
+ safeRm(rawPath);
262
+ return [];
263
+ }
264
+ writeJson(path.join(rawPath, "manifest.json"), {
265
+ version: 1,
266
+ copiedAt: new Date().toISOString(),
267
+ files: records
268
+ });
269
+ return records;
270
+ }
271
+
272
+ function copySharedRawFile(file, env = process.env) {
273
+ const stat = safeStat(file);
274
+ if (!stat || !stat.isFile()) return null;
275
+ const resolved = path.resolve(file);
276
+ const cacheKey = `${resolved}:${stat.size}:${Math.floor(stat.mtimeMs)}`;
277
+ const cached = SHARED_RAW_SOURCE_CACHE.get(cacheKey);
278
+ if (cached && fs.existsSync(cached.sharedRawPath)) return { ...cached };
279
+ let sha256;
280
+ try {
281
+ sha256 = fileSha256(resolved);
282
+ } catch (error) {
283
+ if (error.code === "ENOENT") return null;
284
+ throw error;
285
+ }
286
+ const filename = `${sha256}-${safeRawFilename(path.basename(file))}`;
287
+ const dir = path.join(archiveRoot(env), "raw-sources", sha256.slice(0, 2));
288
+ ensureDir(dir);
289
+ const sharedRawPath = path.join(dir, filename);
290
+ if (!fs.existsSync(sharedRawPath)) {
291
+ try {
292
+ fs.copyFileSync(resolved, sharedRawPath);
293
+ } catch (error) {
294
+ if (error.code === "ENOENT") return null;
295
+ throw error;
296
+ }
297
+ fs.chmodSync(sharedRawPath, 0o600);
298
+ }
299
+ const record = {
300
+ reference: true,
301
+ originalPath: resolved,
302
+ filename,
303
+ archivedPath: sharedRawPath,
304
+ sharedRawPath,
305
+ size: stat.size,
306
+ mtime: new Date(stat.mtimeMs).toISOString(),
307
+ sha256,
308
+ note: "shared raw source"
309
+ };
310
+ SHARED_RAW_SOURCE_CACHE.set(cacheKey, record);
311
+ return { ...record };
312
+ }
313
+
314
+ function rawSourceFiles(input) {
315
+ const values = [
316
+ ...(Array.isArray(input.rawFiles) ? input.rawFiles : []),
317
+ ...(Array.isArray(input.sourceFiles) ? input.sourceFiles : []),
318
+ input.sourcePath
319
+ ];
320
+ const seen = new Set();
321
+ const files = [];
322
+ for (const value of values) {
323
+ const candidate = rawSourceCandidate(value);
324
+ if (!candidate) continue;
325
+ for (const file of [candidate, ...sqliteSidecarFiles(candidate)]) {
326
+ const stat = safeStat(file);
327
+ if (!stat || !stat.isFile()) continue;
328
+ const resolved = path.resolve(file);
329
+ if (seen.has(resolved)) continue;
330
+ seen.add(resolved);
331
+ files.push(resolved);
332
+ }
333
+ }
334
+ return files;
335
+ }
336
+
337
+ function rawReferences(input) {
338
+ return (Array.isArray(input.rawReferences) ? input.rawReferences : [])
339
+ .filter((item) => item && typeof item === "object")
340
+ .map((item, index) => ({
341
+ reference: true,
342
+ filename: item.filename || `reference-${String(index + 1).padStart(3, "0")}`,
343
+ originalPath: item.originalPath || item.sourcePath || "",
344
+ archivedPath: item.archivedPath || item.sharedRawPath || "",
345
+ sharedRawPath: item.sharedRawPath || item.archivedPath || "",
346
+ entryPath: item.entryPath || "",
347
+ conversationId: item.conversationId || "",
348
+ sha256: item.sha256 || "",
349
+ size: Number.isFinite(Number(item.size)) ? Number(item.size) : undefined,
350
+ mtime: item.mtime || undefined,
351
+ note: item.note || undefined
352
+ }));
353
+ }
354
+
355
+ function rawSourceCandidate(value) {
356
+ const text = String(value || "").trim();
357
+ if (!text) return "";
358
+ if (fs.existsSync(text)) return text;
359
+ const hashIndex = text.indexOf("#");
360
+ if (hashIndex > 0) {
361
+ const beforeHash = text.slice(0, hashIndex);
362
+ if (fs.existsSync(beforeHash)) return beforeHash;
363
+ }
364
+ return "";
365
+ }
366
+
367
+ function sqliteSidecarFiles(file) {
368
+ if (!/\.(sqlite|sqlite3|db|vscdb)$/i.test(file)) return [];
369
+ return [`${file}-wal`, `${file}-shm`];
370
+ }
371
+
372
+ function safeRawFilename(value) {
373
+ return (String(value || "source").replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "") || "source").slice(0, 180);
374
+ }
375
+
376
+ function fileSha256(file) {
377
+ return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
378
+ }
379
+
380
+ function safeUnlink(file) {
381
+ try {
382
+ fs.unlinkSync(file);
383
+ } catch (error) {
384
+ if (error.code !== "ENOENT") throw error;
385
+ }
386
+ }
387
+
388
+ function safeRm(file) {
389
+ try {
390
+ fs.rmSync(file, { recursive: true, force: true });
391
+ } catch (error) {
392
+ if (error.code !== "ENOENT") throw error;
393
+ }
394
+ }
395
+
396
+ function safeStat(file) {
397
+ try {
398
+ return fs.statSync(file);
399
+ } catch {
400
+ return null;
401
+ }
402
+ }
403
+
404
+ function normalizeMessages(messages) {
405
+ const normalized = messages
406
+ .map((message, index) => ({
407
+ type: "message",
408
+ index,
409
+ timestamp: toIso(message.timestamp) || new Date().toISOString(),
410
+ role: normalizeRole(message.role),
411
+ content: String(message.content ?? ""),
412
+ metadata: message.metadata || {}
413
+ }))
414
+ .filter((message) => message.content.trim() || hasStructuredMetadata(message.metadata));
415
+ return dedupeAdjacentMessages(normalized).map((message, index) => ({ ...message, index }));
416
+ }
417
+
418
+ function hasStructuredMetadata(metadata) {
419
+ return Boolean(
420
+ (Array.isArray(metadata?.toolCalls) && metadata.toolCalls.length) ||
421
+ metadata?.toolResult ||
422
+ metadata?.eventType ||
423
+ metadata?.parserVersion
424
+ );
425
+ }
426
+
427
+ function dedupeAdjacentMessages(messages) {
428
+ const result = [];
429
+ for (const message of messages) {
430
+ const previous = result[result.length - 1];
431
+ if (
432
+ previous &&
433
+ previous.role === message.role &&
434
+ normalizedContent(previous.content) === normalizedContent(message.content) &&
435
+ timestampsNear(previous.timestamp, message.timestamp)
436
+ ) {
437
+ continue;
438
+ }
439
+ result.push(message);
440
+ }
441
+ return result;
442
+ }
443
+
444
+ function normalizedContent(value) {
445
+ return String(value || "").replace(/\s+/g, " ").trim();
446
+ }
447
+
448
+ function timestampsNear(left, right) {
449
+ const leftTime = Date.parse(left);
450
+ const rightTime = Date.parse(right);
451
+ if (!Number.isFinite(leftTime) || !Number.isFinite(rightTime)) return String(left || "") === String(right || "");
452
+ return Math.abs(leftTime - rightTime) <= 2000;
453
+ }
454
+
455
+ function normalizeRole(role) {
456
+ const value = String(role || "unknown").toLowerCase();
457
+ if (["user", "assistant", "system", "tool"].includes(value)) return value;
458
+ if (["human"].includes(value)) return "user";
459
+ if (["bot", "agent", "model"].includes(value)) return "assistant";
460
+ return value || "unknown";
461
+ }
462
+
463
+ function toIso(value) {
464
+ if (!value) return "";
465
+ if (typeof value === "number") {
466
+ const ms = value > 1e12 ? value : value * 1000;
467
+ return new Date(ms).toISOString();
468
+ }
469
+ const date = new Date(value);
470
+ return Number.isNaN(date.getTime()) ? "" : date.toISOString();
471
+ }
472
+
473
+ function stableSessionId(provider, sourcePath, startedAt, messages) {
474
+ const hash = crypto
475
+ .createHash("sha256")
476
+ .update(provider)
477
+ .update("\0")
478
+ .update(sourcePath)
479
+ .update("\0")
480
+ .update(startedAt)
481
+ .update("\0")
482
+ .update(JSON.stringify(messages.slice(0, 20)))
483
+ .digest("hex");
484
+ return hash.slice(0, 24);
485
+ }
486
+
487
+ function encodeSegment(value) {
488
+ return encodeURIComponent(value || "unknown").replace(/[!'()*]/g, (char) =>
489
+ `%${char.charCodeAt(0).toString(16).toUpperCase()}`
490
+ );
491
+ }
492
+
493
+ function decodeSegment(value) {
494
+ return decodeURIComponent(value);
495
+ }
496
+
497
+ const _sessionsIndex = {
498
+ byPath: new Map(),
499
+ byId: new Map()
500
+ };
501
+
502
+ function hydrateSessionMetadata(file, metadata) {
503
+ const conversationPath = metadata.conversationPath || file.replace(/\.metadata\.json$/, ".conversation.md");
504
+ const transcriptPath = metadata.transcriptPath || file.replace(/\.metadata\.json$/, ".transcript.jsonl");
505
+ const eventPath = metadata.eventPath || file.replace(/\.metadata\.json$/, ".events.jsonl");
506
+ const viewPath = metadata.viewPath || file.replace(/\.metadata\.json$/, ".view.json");
507
+ const derivedRawPath = file.replace(/\.metadata\.json$/, ".raw");
508
+ const rawPath = metadata.rawPath || (fs.existsSync(derivedRawPath) ? derivedRawPath : "");
509
+ return { ...metadata, conversationPath, metadataPath: file, transcriptPath, eventPath, viewPath, rawPath };
510
+ }
511
+
512
+ function listSessions(env = process.env) {
513
+ const root = path.join(archiveRoot(env), "sessions");
514
+ const seen = new Set();
515
+ const sessions = [];
516
+ walk(root, (file) => {
517
+ if (!file.endsWith(".metadata.json")) return;
518
+ seen.add(file);
519
+ let stat;
520
+ try { stat = fs.statSync(file); } catch { return; }
521
+ const cached = _sessionsIndex.byPath.get(file);
522
+ if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
523
+ sessions.push(cached.session);
524
+ return;
525
+ }
526
+ const metadata = readJson(file, null);
527
+ if (!metadata) return;
528
+ const session = hydrateSessionMetadata(file, metadata);
529
+ _sessionsIndex.byPath.set(file, { mtimeMs: stat.mtimeMs, size: stat.size, session });
530
+ if (session.sessionId) _sessionsIndex.byId.set(session.sessionId, file);
531
+ sessions.push(session);
532
+ });
533
+ for (const key of [..._sessionsIndex.byPath.keys()]) {
534
+ if (!seen.has(key)) {
535
+ const dropped = _sessionsIndex.byPath.get(key);
536
+ if (dropped?.session?.sessionId) _sessionsIndex.byId.delete(dropped.session.sessionId);
537
+ _sessionsIndex.byPath.delete(key);
538
+ }
539
+ }
540
+ return sessions.sort((a, b) => String(b.startedAt).localeCompare(String(a.startedAt)));
541
+ }
542
+
543
+ function findSessionById(sessionId, env = process.env) {
544
+ if (!sessionId) return null;
545
+ const cachedPath = _sessionsIndex.byId.get(sessionId);
546
+ if (cachedPath) {
547
+ const cached = _sessionsIndex.byPath.get(cachedPath);
548
+ if (cached) {
549
+ let stat;
550
+ try { stat = fs.statSync(cachedPath); } catch { stat = null; }
551
+ if (stat && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
552
+ return cached.session;
553
+ }
554
+ if (stat) {
555
+ const metadata = readJson(cachedPath, null);
556
+ if (metadata) {
557
+ const session = hydrateSessionMetadata(cachedPath, metadata);
558
+ _sessionsIndex.byPath.set(cachedPath, { mtimeMs: stat.mtimeMs, size: stat.size, session });
559
+ if (session.sessionId) _sessionsIndex.byId.set(session.sessionId, cachedPath);
560
+ return session;
561
+ }
562
+ }
563
+ _sessionsIndex.byPath.delete(cachedPath);
564
+ _sessionsIndex.byId.delete(sessionId);
565
+ }
566
+ }
567
+ return listSessions(env).find((session) => session.sessionId === sessionId) || null;
568
+ }
569
+
570
+ function computeSessionUsage(messages) {
571
+ let inputTokens = 0;
572
+ let outputTokens = 0;
573
+ let cacheInputTokens = 0;
574
+ let reasoningOutputTokens = 0;
575
+ let toolUsePromptTokens = 0;
576
+ let maxUndifferentiatedTotalTokens = 0;
577
+ let totalInputTokens = 0;
578
+ let totalOutputTokens = 0;
579
+ const seenUsageRequests = new Set();
580
+ let any = false;
581
+ for (const message of messages || []) {
582
+ const usage = message && message.metadata && message.metadata.usage;
583
+ if (!usage || typeof usage !== "object") continue;
584
+ const requestId = String(message.metadata?.requestId || "").trim();
585
+ if (requestId) {
586
+ if (seenUsageRequests.has(requestId)) continue;
587
+ seenUsageRequests.add(requestId);
588
+ }
589
+ const summary = usageTokenSummary(usage);
590
+ if (!summary.hasAny) continue;
591
+ if (summary.inputTokens > 0) {
592
+ inputTokens += summary.inputTokens;
593
+ any = true;
594
+ }
595
+ if (summary.outputTokens > 0) {
596
+ outputTokens += summary.outputTokens;
597
+ any = true;
598
+ }
599
+ if (summary.cacheInputTokens > 0) cacheInputTokens += summary.cacheInputTokens;
600
+ if (summary.reasoningOutputTokens > 0) reasoningOutputTokens += summary.reasoningOutputTokens;
601
+ if (summary.toolUsePromptTokens > 0) toolUsePromptTokens += summary.toolUsePromptTokens;
602
+ if (!summary.hasSplitTokens && !summary.hasExtraTokens && summary.hasTotalTokens) {
603
+ maxUndifferentiatedTotalTokens = Math.max(maxUndifferentiatedTotalTokens, summary.totalTokens);
604
+ }
605
+ if (summary.totalInputTokens > totalInputTokens) totalInputTokens = summary.totalInputTokens;
606
+ if (summary.totalOutputTokens > totalOutputTokens) totalOutputTokens = summary.totalOutputTokens;
607
+ }
608
+ if (Number.isFinite(totalInputTokens) && totalInputTokens > 0) any = true;
609
+ if (Number.isFinite(totalOutputTokens) && totalOutputTokens > 0) any = true;
610
+ if (!any && maxUndifferentiatedTotalTokens > 0) {
611
+ inputTokens = maxUndifferentiatedTotalTokens;
612
+ any = true;
613
+ }
614
+ const cumulativeTokens = totalInputTokens + totalOutputTokens;
615
+ const finalTotalTokens = inputTokens + outputTokens || cumulativeTokens || maxUndifferentiatedTotalTokens;
616
+ if (!any && finalTotalTokens === 0) return null;
617
+ return {
618
+ inputTokens: inputTokens || totalInputTokens || (!outputTokens && finalTotalTokens ? finalTotalTokens : 0),
619
+ outputTokens: outputTokens || totalOutputTokens,
620
+ cacheInputTokens,
621
+ reasoningOutputTokens,
622
+ toolUsePromptTokens,
623
+ totalTokens: finalTotalTokens,
624
+ totalInputTokens: totalInputTokens || inputTokens,
625
+ totalOutputTokens: totalOutputTokens || outputTokens
626
+ };
627
+ }
628
+
629
+ function usageTokenSummary(usage) {
630
+ if (!usage || typeof usage !== "object") return emptyUsageTokenSummary();
631
+ const inputTokens = positiveTokenNumber(firstUsageNumber(
632
+ usage.inputTokens,
633
+ usage.input_tokens,
634
+ usage.promptTokens,
635
+ usage.prompt_tokens,
636
+ usage.promptTokenCount,
637
+ usage.prompt_token_count,
638
+ usage.inputTokenCount,
639
+ usage.input_token_count,
640
+ usage.prompt,
641
+ usage.input
642
+ ));
643
+ const outputTokens = positiveTokenNumber(firstUsageNumber(
644
+ usage.outputTokens,
645
+ usage.output_tokens,
646
+ usage.completionTokens,
647
+ usage.completion_tokens,
648
+ usage.completionTokenCount,
649
+ usage.completion_token_count,
650
+ usage.candidatesTokens,
651
+ usage.candidates_tokens,
652
+ usage.candidatesTokenCount,
653
+ usage.candidates_token_count,
654
+ usage.outputTokenCount,
655
+ usage.output_token_count,
656
+ usage.completion,
657
+ usage.output
658
+ ));
659
+ const totalInputTokens = positiveTokenNumber(firstUsageNumber(
660
+ usage.totalInputTokens,
661
+ usage.total_input_tokens,
662
+ usage.totalPromptTokens,
663
+ usage.total_prompt_tokens,
664
+ usage.totalPromptTokenCount,
665
+ usage.total_prompt_token_count
666
+ ));
667
+ const totalOutputTokens = positiveTokenNumber(firstUsageNumber(
668
+ usage.totalOutputTokens,
669
+ usage.total_output_tokens,
670
+ usage.totalCompletionTokens,
671
+ usage.total_completion_tokens,
672
+ usage.totalCompletionTokenCount,
673
+ usage.total_completion_token_count
674
+ ));
675
+ const explicitTotalTokens = positiveTokenNumber(firstUsageNumber(
676
+ usage.totalTokens,
677
+ usage.total_tokens,
678
+ usage.totalTokenCount,
679
+ usage.total_token_count,
680
+ usage.total
681
+ ));
682
+ const cacheInputTokens = sumPositiveTokenNumbers(
683
+ firstUsageNumber(usage.cacheInputTokens, usage.cache_input_tokens),
684
+ firstUsageNumber(usage.cacheCreationInputTokens, usage.cache_creation_input_tokens),
685
+ firstUsageNumber(usage.cacheReadInputTokens, usage.cache_read_input_tokens),
686
+ firstUsageNumber(usage.cacheReadTokens, usage.cache_read_tokens),
687
+ firstUsageNumber(usage.cachedContentTokenCount, usage.cached_content_token_count),
688
+ firstUsageNumber(usage.cachedTokens, usage.cached_tokens, usage.cacheTokens, usage.cache_tokens, usage.cached)
689
+ );
690
+ const reasoningOutputTokens = sumPositiveTokenNumbers(
691
+ firstUsageNumber(usage.reasoningOutputTokens, usage.reasoning_output_tokens),
692
+ firstUsageNumber(usage.thoughtsTokens, usage.thoughts_tokens, usage.thoughtsTokenCount, usage.thoughts_token_count),
693
+ firstUsageNumber(usage.reasoningTokens, usage.reasoning_tokens, usage.reasoningTokenCount, usage.reasoning_token_count)
694
+ );
695
+ const toolUsePromptTokens = sumPositiveTokenNumbers(
696
+ firstUsageNumber(usage.toolUsePromptTokens, usage.tool_use_prompt_tokens, usage.toolUsePromptTokenCount, usage.tool_use_prompt_token_count)
697
+ );
698
+ const extraTokens = cacheInputTokens + reasoningOutputTokens + toolUsePromptTokens;
699
+ const splitTokens = inputTokens + outputTokens;
700
+ const cumulativeTokens = totalInputTokens + totalOutputTokens;
701
+ const categoryTokens = splitTokens + extraTokens;
702
+ const totalTokens = explicitTotalTokens || categoryTokens
703
+ ? Math.max(explicitTotalTokens, categoryTokens)
704
+ : cumulativeTokens;
705
+ return {
706
+ inputTokens,
707
+ outputTokens,
708
+ totalInputTokens,
709
+ totalOutputTokens,
710
+ cacheInputTokens,
711
+ reasoningOutputTokens,
712
+ toolUsePromptTokens,
713
+ extraTokens,
714
+ totalTokens,
715
+ hasAny: totalTokens > 0,
716
+ hasSplitTokens: splitTokens > 0,
717
+ hasExtraTokens: extraTokens > 0,
718
+ hasTotalTokens: explicitTotalTokens > 0,
719
+ hasCumulativeTokens: cumulativeTokens > 0
720
+ };
721
+ }
722
+
723
+ function emptyUsageTokenSummary() {
724
+ return {
725
+ inputTokens: 0,
726
+ outputTokens: 0,
727
+ totalInputTokens: 0,
728
+ totalOutputTokens: 0,
729
+ cacheInputTokens: 0,
730
+ reasoningOutputTokens: 0,
731
+ toolUsePromptTokens: 0,
732
+ extraTokens: 0,
733
+ totalTokens: 0,
734
+ hasAny: false,
735
+ hasSplitTokens: false,
736
+ hasExtraTokens: false,
737
+ hasTotalTokens: false,
738
+ hasCumulativeTokens: false
739
+ };
740
+ }
741
+
742
+ function firstUsageNumber(...values) {
743
+ for (const value of values) {
744
+ if (value === null || value === undefined || value === "") continue;
745
+ const number = Number(value);
746
+ if (Number.isFinite(number)) return number;
747
+ }
748
+ return 0;
749
+ }
750
+
751
+ function positiveTokenNumber(value) {
752
+ const number = Number(value);
753
+ return Number.isFinite(number) && number > 0 ? number : 0;
754
+ }
755
+
756
+ function sumPositiveTokenNumbers(...values) {
757
+ return values.reduce((sum, value) => sum + positiveTokenNumber(value), 0);
758
+ }
759
+
760
+ function collectSessionModels(messages) {
761
+ const models = new Set();
762
+ for (const message of messages || []) {
763
+ const model = message && message.metadata && message.metadata.model;
764
+ if (typeof model === "string" && model.trim()) models.add(model.trim());
765
+ }
766
+ return Array.from(models);
767
+ }
768
+
769
+ function readTranscript(file) {
770
+ const messages = [];
771
+ try {
772
+ const text = fs.readFileSync(file, "utf8");
773
+ for (const line of text.split(/\r?\n/)) {
774
+ if (!line.trim()) continue;
775
+ messages.push(JSON.parse(line));
776
+ }
777
+ } catch (error) {
778
+ if (error.code !== "ENOENT") throw error;
779
+ }
780
+ return messages;
781
+ }
782
+
783
+ function readEvents(sessionOrPath) {
784
+ const file = typeof sessionOrPath === "string"
785
+ ? sessionOrPath
786
+ : sessionOrPath?.eventPath || sessionOrPath?.metadataPath?.replace(/\.metadata\.json$/, ".events.jsonl") || "";
787
+ const events = [];
788
+ if (!file) return events;
789
+ try {
790
+ const text = fs.readFileSync(file, "utf8");
791
+ for (const line of text.split(/\r?\n/)) {
792
+ if (!line.trim()) continue;
793
+ events.push(JSON.parse(line));
794
+ }
795
+ } catch (error) {
796
+ if (error.code !== "ENOENT") throw error;
797
+ }
798
+ return events;
799
+ }
800
+
801
+ function ensureConversationMarkdown(session, env = process.env) {
802
+ const conversationPath = session.conversationPath || session.metadataPath?.replace(/\.metadata\.json$/, ".conversation.md");
803
+ if (!conversationPath) return "";
804
+ if (fs.existsSync(conversationPath)) return conversationPath;
805
+ const messages = readTranscript(session.transcriptPath);
806
+ if (!messages.length) return "";
807
+ const events = readEvents(session);
808
+ ensureDir(path.dirname(conversationPath));
809
+ fs.writeFileSync(conversationPath, renderConversationMarkdown(session, messages, events), { mode: 0o600 });
810
+ if (session.metadataPath) {
811
+ const metadata = readJson(session.metadataPath, null);
812
+ if (metadata) writeJson(session.metadataPath, { ...metadata, conversationPath });
813
+ }
814
+ return conversationPath;
815
+ }
816
+
817
+ const VIEW_SCHEMA_VERSION = 1;
818
+
819
+ function sessionViewPathFromMetadata(metadataPath) {
820
+ if (!metadataPath) return "";
821
+ return metadataPath.replace(/\.metadata\.json$/, ".view.json");
822
+ }
823
+
824
+ function sessionViewPathForSession(session) {
825
+ return session.viewPath || sessionViewPathFromMetadata(session.metadataPath);
826
+ }
827
+
828
+ function dedupeTranscriptMessages(messages) {
829
+ const result = [];
830
+ for (const message of messages || []) {
831
+ const previous = result[result.length - 1];
832
+ if (
833
+ previous &&
834
+ previous.role === message.role &&
835
+ normalizedTranscriptContent(previous.content) === normalizedTranscriptContent(message.content) &&
836
+ transcriptTimestampsNear(previous.timestamp, message.timestamp)
837
+ ) {
838
+ continue;
839
+ }
840
+ result.push(message);
841
+ }
842
+ return result;
843
+ }
844
+
845
+ function normalizedTranscriptContent(value) {
846
+ return String(value || "").replace(/\s+/g, " ").trim();
847
+ }
848
+
849
+ function transcriptTimestampsNear(left, right) {
850
+ const leftTime = Date.parse(left);
851
+ const rightTime = Date.parse(right);
852
+ if (!Number.isFinite(leftTime) || !Number.isFinite(rightTime)) return String(left || "") === String(right || "");
853
+ return Math.abs(leftTime - rightTime) <= 2000;
854
+ }
855
+
856
+ function displayTranscriptMessages(messages, time) {
857
+ if (time?.status !== "recovered-time-unknown") return messages;
858
+ return messages.map((message) => ({ ...message, timestamp: "" }));
859
+ }
860
+
861
+ function cursorRawTimeMatchesSourceMtime(session, timestamp) {
862
+ const rawFiles = Array.isArray(session?.rawFiles) ? session.rawFiles : [];
863
+ return rawFiles.some((file) => {
864
+ const mtime = Date.parse(file?.mtime || "");
865
+ if (!Number.isFinite(mtime)) return false;
866
+ const near = Math.abs(timestamp - mtime) <= 2 * 60 * 60 * 1000;
867
+ const looksLikeCursorDb = /state\.vscdb(?:\.backup|-wal)?$/.test(String(file.originalPath || file.filename || ""));
868
+ return looksLikeCursorDb && near;
869
+ });
870
+ }
871
+
872
+ function cursorRawSyntheticTimeParts(session) {
873
+ const result = { startSynthetic: false, endSynthetic: false };
874
+ if (!session || session.provider !== "cursor") return result;
875
+ if (!["cursor-raw-sqlite-salvage", "cursor-workspace-sqlite"].includes(session.sourceType)) return result;
876
+ const started = Date.parse(session.startedAt || "");
877
+ const ended = Date.parse(session.endedAt || session.startedAt || "");
878
+ result.startSynthetic = Number.isFinite(started) && cursorRawTimeMatchesSourceMtime(session, started);
879
+ result.endSynthetic = Number.isFinite(ended) && cursorRawTimeMatchesSourceMtime(session, ended);
880
+ return result;
881
+ }
882
+
883
+ function sessionHistoryTime(session) {
884
+ const rawTime = cursorRawSyntheticTimeParts(session);
885
+ if (rawTime.startSynthetic && rawTime.endSynthetic) {
886
+ return { startedAt: "", endedAt: "", status: "recovered-time-unknown" };
887
+ }
888
+ if (rawTime.endSynthetic) {
889
+ return { startedAt: session?.startedAt || "", endedAt: "", status: "recovered-partial-time" };
890
+ }
891
+ return { startedAt: session?.startedAt || "", endedAt: session?.endedAt || "", status: "" };
892
+ }
893
+
894
+ function computeSessionView(session, transcriptMessages, canonicalEvents) {
895
+ const time = sessionHistoryTime(session);
896
+ const display = displayTranscriptMessages(dedupeTranscriptMessages(transcriptMessages || []), time);
897
+ return {
898
+ view_schema_version: VIEW_SCHEMA_VERSION,
899
+ session_id: session?.sessionId || "",
900
+ started_at: time.startedAt || "",
901
+ ended_at: time.endedAt || "",
902
+ time_status: time.status || "",
903
+ transcript_messages: display,
904
+ canonical_events: Array.isArray(canonicalEvents) ? canonicalEvents : []
905
+ };
906
+ }
907
+
908
+ function writeSessionView(viewPath, baked) {
909
+ ensureDir(path.dirname(viewPath));
910
+ fs.writeFileSync(viewPath, JSON.stringify(baked), { mode: 0o600 });
911
+ }
912
+
913
+ function ensureSessionView(session, env = process.env) {
914
+ const viewPath = sessionViewPathForSession(session);
915
+ if (viewPath && fs.existsSync(viewPath)) {
916
+ const cached = readJson(viewPath, null);
917
+ if (cached && cached.view_schema_version === VIEW_SCHEMA_VERSION) return cached;
918
+ }
919
+ const baked = computeSessionView(
920
+ session,
921
+ readTranscript(session.transcriptPath),
922
+ readEvents(session)
923
+ );
924
+ if (viewPath) {
925
+ try {
926
+ writeSessionView(viewPath, baked);
927
+ if (session.metadataPath) {
928
+ const metadata = readJson(session.metadataPath, null);
929
+ if (metadata && (metadata.viewPath !== viewPath || metadata.viewSchemaVersion !== VIEW_SCHEMA_VERSION)) {
930
+ writeJson(session.metadataPath, { ...metadata, viewPath, viewSchemaVersion: VIEW_SCHEMA_VERSION });
931
+ }
932
+ }
933
+ } catch (error) {
934
+ if (error.code !== "EACCES" && error.code !== "EROFS") throw error;
935
+ }
936
+ }
937
+ return baked;
938
+ }
939
+
940
+ function renderConversationMarkdown(session, messages, events = []) {
941
+ const title = session.title || `${session.provider || "agent"} session ${session.sessionId}`;
942
+ const hasRedactions =
943
+ messages.some((message) => /\[REDACTED:[^\]\n]+\]/.test(JSON.stringify(message))) ||
944
+ events.some((event) => /\[REDACTED:[^\]\n]+\]/.test(JSON.stringify(event)));
945
+ const frontmatter = [
946
+ "---",
947
+ `session_id: ${yamlString(session.sessionId)}`,
948
+ `provider: ${yamlString(session.provider)}`,
949
+ `repo: ${yamlString(session.repoCanonical || "")}`,
950
+ `scope: ${yamlString(session.scopeCanonical || "")}`,
951
+ `cwd: ${yamlString(session.cwd || "")}`,
952
+ `started_at: ${yamlString(session.startedAt || "")}`,
953
+ `ended_at: ${yamlString(session.endedAt || "")}`,
954
+ `source_type: ${yamlString(session.sourceType || "")}`,
955
+ `storage_scope: ${yamlString(session.storageScope || "")}`,
956
+ `chat_account_id: ${yamlString(session.chatAccountId || "")}`,
957
+ `chat_display_name: ${yamlString(session.chatDisplayName || "")}`,
958
+ `chat_project_path: ${yamlString(session.chatProjectPath || "")}`,
959
+ `conversation_kind: ${yamlString(session.conversationKind || "")}`,
960
+ `archive_schema_version: ${yamlString(session.archiveSchemaVersion || 1)}`,
961
+ `event_count: ${yamlString(events.length || session.eventCount || 0)}`,
962
+ `parser_versions: ${yamlString(JSON.stringify(session.parserVersions || {}))}`,
963
+ "---",
964
+ ""
965
+ ];
966
+ const body = [`# ${markdownLine(title)}`, ""];
967
+ if (hasRedactions) {
968
+ body.push(
969
+ "<style>",
970
+ ".agentlog-redaction{display:inline-block;padding:0 0.35em;border-radius:0.35em;background:#fff1f2;color:#9f1239;border:1px solid #fecdd3;font-weight:650;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.92em;}",
971
+ "</style>",
972
+ "",
973
+ "> agentlog redactions are highlighted where sensitive text was removed.",
974
+ ""
975
+ );
976
+ }
977
+ for (const message of messages) {
978
+ body.push(`## ${roleLabel(message.role)} - ${message.timestamp || "unknown time"}`);
979
+ body.push("");
980
+ body.push(styleRedactionMarkersForMarkdown(String(message.content || "").trim()));
981
+ body.push("");
982
+ for (const event of events.filter((item) => item.messageIndex === message.index && item.kind === "tool.called")) {
983
+ body.push(`### Tool Call - ${event.occurredAt || message.timestamp || "unknown time"}`);
984
+ body.push("");
985
+ body.push(styleRedactionMarkersForMarkdown(String(event.body?.text || event.indexed?.summary || "").trim()));
986
+ body.push("");
987
+ }
988
+ for (const event of events.filter((item) => item.messageIndex === message.index && item.kind === "tool.completed")) {
989
+ body.push(`### Tool Result - ${event.occurredAt || message.timestamp || "unknown time"}`);
990
+ body.push("");
991
+ body.push(styleRedactionMarkersForMarkdown(String(event.indexed?.summary || event.body?.text || "").trim()));
992
+ body.push("");
993
+ }
994
+ }
995
+ return `${frontmatter.concat(body).join("\n").trimEnd()}\n`;
996
+ }
997
+
998
+ function normalizePrecomputedEvents(events, session) {
999
+ return events
1000
+ .filter((event) => event && typeof event === "object")
1001
+ .map((event) => ({
1002
+ ...event,
1003
+ sessionId: event.sessionId || session.sessionId,
1004
+ provider: event.provider || session.provider,
1005
+ sourceType: event.sourceType || session.sourceType,
1006
+ parserVersion: event.parserVersion ?? session.parserVersion ?? null,
1007
+ repoCanonical: event.repoCanonical ?? session.repoCanonical ?? "",
1008
+ scopeCanonical: event.scopeCanonical ?? session.scopeCanonical ?? ""
1009
+ }));
1010
+ }
1011
+
1012
+ function redactCanonicalEvents(events, options) {
1013
+ return events.map((event) => redactStructuredStrings(event, options));
1014
+ }
1015
+
1016
+ function redactStructuredStrings(value, options) {
1017
+ if (typeof value === "string") {
1018
+ const result = redactText(value, options);
1019
+ mergeSummaries(options.summary, result.summary);
1020
+ return result.text;
1021
+ }
1022
+ if (Array.isArray(value)) return value.map((item) => redactStructuredStrings(item, options));
1023
+ if (value && typeof value === "object") {
1024
+ const output = {};
1025
+ for (const [key, item] of Object.entries(value)) {
1026
+ output[key] = redactStructuredStrings(item, options);
1027
+ }
1028
+ return output;
1029
+ }
1030
+ return value;
1031
+ }
1032
+
1033
+ function yamlString(value) {
1034
+ return JSON.stringify(String(value ?? ""));
1035
+ }
1036
+
1037
+ function markdownLine(value) {
1038
+ return String(value || "(untitled session)").replace(/\r?\n/g, " ").trim();
1039
+ }
1040
+
1041
+ function roleLabel(role) {
1042
+ const value = String(role || "unknown").trim();
1043
+ return value ? value[0].toUpperCase() + value.slice(1) : "Unknown";
1044
+ }
1045
+
1046
+ function touchManifest(session, pathsForSession, env = process.env) {
1047
+ const manifestPath = path.join(archiveRoot(env), "sessions", "manifest.json");
1048
+ const manifest = readJson(manifestPath, { sessions: [] });
1049
+ const next = manifest.sessions.filter((item) => item.sessionId !== session.sessionId);
1050
+ next.push({
1051
+ sessionId: session.sessionId,
1052
+ provider: session.provider,
1053
+ repoCanonical: session.repoCanonical,
1054
+ scopeCanonical: session.scopeCanonical,
1055
+ startedAt: session.startedAt,
1056
+ conversationPath: pathsForSession.conversationPath,
1057
+ metadataPath: pathsForSession.metadataPath,
1058
+ transcriptPath: pathsForSession.transcriptPath,
1059
+ eventPath: pathsForSession.eventPath,
1060
+ viewPath: pathsForSession.viewPath || "",
1061
+ rawPath: pathsForSession.rawPath || ""
1062
+ });
1063
+ writeJson(manifestPath, { sessions: next.sort((a, b) => String(b.startedAt).localeCompare(String(a.startedAt))) });
1064
+ }
1065
+
1066
+ function countSessions(env = process.env) {
1067
+ const sessions = listSessions(env);
1068
+ const byProvider = {};
1069
+ for (const session of sessions) {
1070
+ byProvider[session.provider] = (byProvider[session.provider] || 0) + 1;
1071
+ }
1072
+ return { total: sessions.length, byProvider };
1073
+ }
1074
+
1075
+ function revealSession(sessionId, env = process.env) {
1076
+ const root = paths(env).revealCache;
1077
+ let found = "";
1078
+ walk(root, (file) => {
1079
+ if (!found && path.basename(file) === `${sessionId}.jsonl`) found = file;
1080
+ });
1081
+ if (!found) return null;
1082
+ return fs.readFileSync(found, "utf8");
1083
+ }
1084
+
1085
+ function walk(dir, visit) {
1086
+ let entries;
1087
+ try {
1088
+ entries = fs.readdirSync(dir, { withFileTypes: true });
1089
+ } catch (error) {
1090
+ if (error.code === "ENOENT") return;
1091
+ throw error;
1092
+ }
1093
+ for (const entry of entries) {
1094
+ const full = path.join(dir, entry.name);
1095
+ if (entry.isDirectory()) walk(full, visit);
1096
+ else visit(full);
1097
+ }
1098
+ }
1099
+
1100
+ module.exports = {
1101
+ archiveRoot,
1102
+ collectSessionModels,
1103
+ computeSessionUsage,
1104
+ computeSessionView,
1105
+ countSessions,
1106
+ decodeSegment,
1107
+ dedupeTranscriptMessages,
1108
+ deleteSessionArchive,
1109
+ displayTranscriptMessages,
1110
+ encodeSegment,
1111
+ ensureConversationMarkdown,
1112
+ ensureSessionView,
1113
+ findSessionById,
1114
+ listSessions,
1115
+ normalizeMessages,
1116
+ objectPathForSession,
1117
+ readEvents,
1118
+ readTranscript,
1119
+ renderConversationMarkdown,
1120
+ revealSession,
1121
+ sessionHistoryTime,
1122
+ sessionViewPathForSession,
1123
+ sessionViewPathFromMetadata,
1124
+ stableSessionId,
1125
+ toIso,
1126
+ usageTokenSummary,
1127
+ VIEW_SCHEMA_VERSION,
1128
+ walk,
1129
+ writeSession
1130
+ };