agentel 0.2.8 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +238 -68
- package/docs/code-reference.md +165 -37
- package/docs/history-source-handling.md +555 -124
- package/docs/release.md +35 -8
- package/npm-shrinkwrap.json +478 -0
- package/package.json +18 -5
- package/scripts/postinstall.js +156 -0
- package/src/archive.js +1176 -65
- package/src/canonical-events.js +346 -35
- package/src/cli.js +7801 -874
- package/src/collector.js +42 -4
- package/src/config.js +51 -4
- package/src/diffs.js +156 -0
- package/src/doctor.js +48 -5
- package/src/importers/claude.js +51 -4
- package/src/importers/copilot.js +385 -0
- package/src/importers/cursor-recovery.js +22 -0
- package/src/importers/factory.js +396 -0
- package/src/importers/gemini.js +39 -0
- package/src/importers/grok.js +367 -0
- package/src/importers/pi.js +422 -0
- package/src/importers/providers.js +64 -5
- package/src/importers.js +4524 -383
- package/src/mcp.js +1 -0
- package/src/memory-sources.js +671 -0
- package/src/memory-store.js +0 -0
- package/src/parser-versions.js +13 -0
- package/src/pricing.js +84 -0
- package/src/search.js +256 -70
- package/src/session-store.js +405 -0
- package/src/slack-notify.js +732 -0
- package/src/source-watch.js +293 -0
- package/src/sources.js +60 -11
- package/src/supervisor.js +231 -7
- package/src/sync.js +6 -0
- package/src/unavailable-sources.js +358 -0
package/src/archive.js
CHANGED
|
@@ -4,11 +4,13 @@ const crypto = require("crypto");
|
|
|
4
4
|
const fs = require("fs");
|
|
5
5
|
const path = require("path");
|
|
6
6
|
const { loadConfig } = require("./config");
|
|
7
|
-
const { normalizeSessionEvents } = require("./canonical-events");
|
|
7
|
+
const { memoryPathInfo, normalizeSessionEvents } = require("./canonical-events");
|
|
8
8
|
const { parserVersionForSource } = require("./parser-versions");
|
|
9
9
|
const { canonicalRepo } = require("./repo");
|
|
10
10
|
const { loadRedactionConfig, loadEnvValues, mergeSummaries, redactText, styleRedactionMarkersForMarkdown } = require("./redaction");
|
|
11
11
|
const { ensureBaseDirs, ensureDir, paths, readJson, writeJson } = require("./paths");
|
|
12
|
+
const sessionStore = require("./session-store");
|
|
13
|
+
const { enqueueSlackNotification } = require("./slack-notify");
|
|
12
14
|
|
|
13
15
|
const SHARED_RAW_SOURCE_CACHE = new Map();
|
|
14
16
|
const ESTIMATED_TOKEN_CHARS = 4;
|
|
@@ -21,12 +23,20 @@ const CURSOR_ESTIMATED_TOKEN_RATES = {
|
|
|
21
23
|
unknown: { inputPerAssistantTurn: 6900, outputPerAssistantTurn: 270 },
|
|
22
24
|
default: { inputPerAssistantTurn: 8400, outputPerAssistantTurn: 230 }
|
|
23
25
|
};
|
|
26
|
+
const ARCHIVE_SCHEMA_VERSION = 6;
|
|
27
|
+
const SESSION_LIST_INDEX_VERSION = 1;
|
|
28
|
+
const OUTPUT_TOKEN_WORK_VERSION = 1;
|
|
29
|
+
const SESSION_OUTCOMES_VERSION = 2;
|
|
24
30
|
|
|
25
31
|
function archiveRoot(env = process.env) {
|
|
26
32
|
const cfg = loadConfig(env);
|
|
27
33
|
return path.join(cfg.storage.root || paths(env).data, "agentlog");
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
function sessionListIndexPath(env = process.env) {
|
|
37
|
+
return path.join(archiveRoot(env), "indexes", "sessions", "sessions.json");
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
function objectPathForSession(session, env = process.env) {
|
|
31
41
|
const started = new Date(session.startedAt || Date.now());
|
|
32
42
|
const year = String(started.getUTCFullYear()).padStart(4, "0");
|
|
@@ -107,9 +117,10 @@ function writeSession(input, env = process.env) {
|
|
|
107
117
|
|
|
108
118
|
const session = {
|
|
109
119
|
version: 1,
|
|
110
|
-
archiveSchemaVersion:
|
|
120
|
+
archiveSchemaVersion: ARCHIVE_SCHEMA_VERSION,
|
|
111
121
|
sessionId,
|
|
112
122
|
provider,
|
|
123
|
+
device: archiveDeviceMetadata(cfg.device),
|
|
113
124
|
scopeCanonical: input.scopeCanonical || "",
|
|
114
125
|
repoCanonical,
|
|
115
126
|
repoSource: input.scopeCanonical ? "scope" : repoInfo.source,
|
|
@@ -168,6 +179,19 @@ function writeSession(input, env = process.env) {
|
|
|
168
179
|
}
|
|
169
180
|
);
|
|
170
181
|
session.eventCount = events.length;
|
|
182
|
+
const toolUsage = collectSessionToolUsage(events);
|
|
183
|
+
if (toolUsage.callCount) session.toolUsage = toolUsage;
|
|
184
|
+
const outputTokenWork = computeOutputTokenWork(archivedMessages);
|
|
185
|
+
if (outputTokenWork && outputTokenWork.totalTokens) session.outputTokenWork = outputTokenWork;
|
|
186
|
+
const outcomes = collectSessionOutcomes(events);
|
|
187
|
+
if (outcomes && (outcomes.editToolCalls || outcomes.filesTouched || outcomes.knowledgeCaptures || outcomes.memoryReads || outcomes.memoryWrites || outcomes.memoryLoads)) session.outcomes = outcomes;
|
|
188
|
+
const artifactsPath = path.join(dir, `session=${sessionId}.artifacts`);
|
|
189
|
+
const artifacts = captureSessionArtifacts(events, artifactsPath, cfg);
|
|
190
|
+
if (artifacts.length) {
|
|
191
|
+
session.artifactsPath = artifactsPath;
|
|
192
|
+
session.artifacts = artifacts;
|
|
193
|
+
session.artifactCount = artifacts.length;
|
|
194
|
+
}
|
|
171
195
|
removeStaleSessionCopies(session, metadataPath, env, { replaceSourcePathCopies: input.replaceSourcePathCopies !== false });
|
|
172
196
|
const rawFiles = copyRawFiles(input, rawPath, env);
|
|
173
197
|
if (rawFiles.length) {
|
|
@@ -206,44 +230,167 @@ function writeSession(input, env = process.env) {
|
|
|
206
230
|
);
|
|
207
231
|
}
|
|
208
232
|
|
|
209
|
-
touchManifest(session, { conversationPath, metadataPath, transcriptPath, eventPath, viewPath, rawPath: session.rawPath || "" }, env);
|
|
210
|
-
|
|
233
|
+
touchManifest(session, { conversationPath, metadataPath, transcriptPath, eventPath, viewPath, rawPath: session.rawPath || "", artifactsPath: session.artifactsPath || "" }, env);
|
|
234
|
+
updateSessionListIndexAfterWrite(session, metadataPath, env, {
|
|
235
|
+
replaceSourcePathCopies: input.replaceSourcePathCopies !== false
|
|
236
|
+
});
|
|
237
|
+
invalidateSessionsSnapshotCache();
|
|
238
|
+
enqueueSlackNotification({ ...session, metadataPath }, cfg, env);
|
|
239
|
+
return { session, conversationPath, metadataPath, transcriptPath, eventPath, viewPath, rawPath: session.rawPath || "", artifactsPath: session.artifactsPath || "" };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function archiveDeviceMetadata(device = {}) {
|
|
243
|
+
return {
|
|
244
|
+
id: String(device.id || ""),
|
|
245
|
+
name: String(device.name || ""),
|
|
246
|
+
slug: String(device.slug || "")
|
|
247
|
+
};
|
|
211
248
|
}
|
|
212
249
|
|
|
213
250
|
function removeStaleSessionCopies(session, metadataPath, env = process.env, options = {}) {
|
|
214
251
|
const root = path.join(archiveRoot(env), "sessions");
|
|
215
252
|
const replaceSourcePathCopies = options.replaceSourcePathCopies !== false;
|
|
253
|
+
if (_sessionsIndex.completeRootKey === root) {
|
|
254
|
+
for (const [file, cached] of [..._sessionsIndex.byPath.entries()]) {
|
|
255
|
+
if (path.resolve(file) === path.resolve(metadataPath)) continue;
|
|
256
|
+
const metadata = cached?.session;
|
|
257
|
+
if (!staleSessionMatches(metadata, session, replaceSourcePathCopies)) continue;
|
|
258
|
+
removeSessionArchiveFiles(metadata, file);
|
|
259
|
+
forgetSessionIndexPath(file);
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
_sessionsIndex.byPath.clear();
|
|
264
|
+
_sessionsIndex.byId.clear();
|
|
216
265
|
walk(root, (file) => {
|
|
217
266
|
if (!file.endsWith(".metadata.json") || path.resolve(file) === path.resolve(metadataPath)) return;
|
|
267
|
+
const stat = safeStat(file);
|
|
268
|
+
if (!stat) return;
|
|
218
269
|
const metadata = readJson(file, null);
|
|
219
|
-
if (!metadata
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
safeUnlink(metadata.viewPath || file.replace(/\.metadata\.json$/, ".view.json"));
|
|
227
|
-
safeRm(metadata.rawPath || file.replace(/\.metadata\.json$/, ".raw"));
|
|
228
|
-
safeUnlink(file);
|
|
270
|
+
if (!metadata) return;
|
|
271
|
+
const hydrated = hydrateSessionMetadata(file, metadata);
|
|
272
|
+
if (!staleSessionMatches(hydrated, session, replaceSourcePathCopies)) {
|
|
273
|
+
rememberSessionIndexEntry(sessionListEntry(hydrated, file, stat));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
removeSessionArchiveFiles(hydrated, file);
|
|
229
277
|
});
|
|
278
|
+
_sessionsIndex.completeRootKey = root;
|
|
230
279
|
}
|
|
231
280
|
|
|
232
|
-
function
|
|
233
|
-
if (!
|
|
234
|
-
|
|
281
|
+
function staleSessionMatches(metadata, session, replaceSourcePathCopies) {
|
|
282
|
+
if (!metadata || metadata.provider !== session.provider) return false;
|
|
283
|
+
if (metadata.sessionId === session.sessionId) return true;
|
|
284
|
+
return Boolean(replaceSourcePathCopies && session.sourcePath && metadata.sourcePath === session.sourcePath);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function removeSessionArchiveFiles(session, metadataPath) {
|
|
235
288
|
safeUnlink(session.conversationPath || metadataPath.replace(/\.metadata\.json$/, ".conversation.md"));
|
|
236
289
|
safeUnlink(session.transcriptPath || metadataPath.replace(/\.metadata\.json$/, ".transcript.jsonl"));
|
|
237
290
|
safeUnlink(session.eventPath || metadataPath.replace(/\.metadata\.json$/, ".events.jsonl"));
|
|
238
291
|
safeUnlink(session.viewPath || metadataPath.replace(/\.metadata\.json$/, ".view.json"));
|
|
239
292
|
safeRm(session.rawPath || metadataPath.replace(/\.metadata\.json$/, ".raw"));
|
|
293
|
+
safeRm(session.artifactsPath || metadataPath.replace(/\.metadata\.json$/, ".artifacts"));
|
|
240
294
|
safeUnlink(metadataPath);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function deleteSessionArchive(session, env = process.env) {
|
|
298
|
+
if (!session || typeof session !== "object") return false;
|
|
299
|
+
const metadataPath = session.metadataPath || "";
|
|
300
|
+
removeSessionArchiveFiles(session, metadataPath);
|
|
241
301
|
if (session.provider && session.sessionId) {
|
|
242
302
|
safeUnlink(path.join(paths(env).revealCache, session.provider, `${session.sessionId}.jsonl`));
|
|
243
303
|
}
|
|
304
|
+
forgetSessionIndexPath(metadataPath);
|
|
305
|
+
removeSessionFromListIndex(session, env);
|
|
306
|
+
invalidateSessionsSnapshotCache();
|
|
244
307
|
return true;
|
|
245
308
|
}
|
|
246
309
|
|
|
310
|
+
const ARTIFACT_PATH_RE = /(?:\/private)?\/(?:tmp|var\/folders)\/[A-Za-z0-9@%+=:,.~_/-]+\.(?:png|jpe?g|gif|webp|svg|pdf)\b/gi;
|
|
311
|
+
|
|
312
|
+
function collectArtifactPathRefs(events) {
|
|
313
|
+
const refs = new Set();
|
|
314
|
+
for (const event of Array.isArray(events) ? events : []) {
|
|
315
|
+
const body = event?.body;
|
|
316
|
+
if (!body) continue;
|
|
317
|
+
const text = JSON.stringify(body);
|
|
318
|
+
if (!text || (!text.includes("/tmp/") && !text.includes("/var/folders/"))) continue;
|
|
319
|
+
let match;
|
|
320
|
+
ARTIFACT_PATH_RE.lastIndex = 0;
|
|
321
|
+
while ((match = ARTIFACT_PATH_RE.exec(text))) refs.add(match[0]);
|
|
322
|
+
}
|
|
323
|
+
return [...refs];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Copy ephemeral files referenced by tool calls (screenshots and PDFs in
|
|
328
|
+
* temp directories) into the session archive before the OS deletes them.
|
|
329
|
+
* Files are content-addressed (sha256-prefixed names) because agents reuse
|
|
330
|
+
* paths like /tmp/screenshot.png across calls and sessions with different
|
|
331
|
+
* content. Previously captured artifacts are kept across re-archives even
|
|
332
|
+
* when the source files are gone or the setting is later disabled.
|
|
333
|
+
*/
|
|
334
|
+
function captureSessionArtifacts(events, artifactsPath, cfg) {
|
|
335
|
+
const settings = cfg?.artifacts || {};
|
|
336
|
+
const manifestFile = path.join(artifactsPath, "artifacts.json");
|
|
337
|
+
const previous = readJson(manifestFile, null);
|
|
338
|
+
const entries = new Map();
|
|
339
|
+
let sessionBytes = 0;
|
|
340
|
+
if (previous && Array.isArray(previous.artifacts)) {
|
|
341
|
+
for (const item of previous.artifacts) {
|
|
342
|
+
if (!item || typeof item.name !== "string" || !item.name) continue;
|
|
343
|
+
entries.set(item.name, item);
|
|
344
|
+
sessionBytes += Number(item.bytes) || 0;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (!settings.enabled) return [...entries.values()];
|
|
348
|
+
const maxFileBytes = Number(settings.maxFileBytes) > 0 ? Number(settings.maxFileBytes) : 25 * 1024 * 1024;
|
|
349
|
+
const maxSessionBytes = Number(settings.maxSessionBytes) > 0 ? Number(settings.maxSessionBytes) : 100 * 1024 * 1024;
|
|
350
|
+
const knownPaths = new Set([...entries.values()].map((item) => `${item.originalPath}:${item.sha256}`));
|
|
351
|
+
let added = false;
|
|
352
|
+
for (const ref of collectArtifactPathRefs(events)) {
|
|
353
|
+
const stat = safeStat(ref);
|
|
354
|
+
if (!stat || !stat.isFile() || stat.size <= 0 || stat.size > maxFileBytes) continue;
|
|
355
|
+
if (sessionBytes + stat.size > maxSessionBytes) continue;
|
|
356
|
+
let content;
|
|
357
|
+
try {
|
|
358
|
+
content = fs.readFileSync(ref);
|
|
359
|
+
} catch {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const sha256 = crypto.createHash("sha256").update(content).digest("hex");
|
|
363
|
+
if (knownPaths.has(`${ref}:${sha256}`)) continue;
|
|
364
|
+
const name = `${sha256.slice(0, 16)}${path.extname(ref).toLowerCase()}`;
|
|
365
|
+
const existing = entries.get(name);
|
|
366
|
+
if (existing) {
|
|
367
|
+
knownPaths.add(`${ref}:${sha256}`);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
ensureDir(artifactsPath);
|
|
372
|
+
fs.writeFileSync(path.join(artifactsPath, name), content, { mode: 0o600 });
|
|
373
|
+
} catch {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
entries.set(name, {
|
|
377
|
+
name,
|
|
378
|
+
originalPath: ref,
|
|
379
|
+
bytes: content.length,
|
|
380
|
+
sha256,
|
|
381
|
+
capturedAt: new Date().toISOString()
|
|
382
|
+
});
|
|
383
|
+
knownPaths.add(`${ref}:${sha256}`);
|
|
384
|
+
sessionBytes += content.length;
|
|
385
|
+
added = true;
|
|
386
|
+
}
|
|
387
|
+
if (added || (entries.size && !previous)) {
|
|
388
|
+
ensureDir(artifactsPath);
|
|
389
|
+
writeJson(manifestFile, { version: 1, artifacts: [...entries.values()] });
|
|
390
|
+
}
|
|
391
|
+
return [...entries.values()];
|
|
392
|
+
}
|
|
393
|
+
|
|
247
394
|
function copyRawFiles(input, rawPath, env = process.env) {
|
|
248
395
|
const files = rawSourceFiles(input);
|
|
249
396
|
const references = rawReferences(input);
|
|
@@ -406,7 +553,20 @@ function safeRawFilename(value) {
|
|
|
406
553
|
}
|
|
407
554
|
|
|
408
555
|
function fileSha256(file) {
|
|
409
|
-
|
|
556
|
+
// Hash in fixed-size chunks; raw session sources can be hundreds of MB and
|
|
557
|
+
// readFileSync would hold the whole file in memory just to hash it.
|
|
558
|
+
const hash = crypto.createHash("sha256");
|
|
559
|
+
const fd = fs.openSync(file, "r");
|
|
560
|
+
try {
|
|
561
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
562
|
+
let bytes = 0;
|
|
563
|
+
while ((bytes = fs.readSync(fd, buffer, 0, buffer.length, null)) > 0) {
|
|
564
|
+
hash.update(buffer.subarray(0, bytes));
|
|
565
|
+
}
|
|
566
|
+
} finally {
|
|
567
|
+
fs.closeSync(fd);
|
|
568
|
+
}
|
|
569
|
+
return hash.digest("hex");
|
|
410
570
|
}
|
|
411
571
|
|
|
412
572
|
function safeUnlink(file) {
|
|
@@ -530,7 +690,8 @@ function decodeSegment(value) {
|
|
|
530
690
|
|
|
531
691
|
const _sessionsIndex = {
|
|
532
692
|
byPath: new Map(),
|
|
533
|
-
byId: new Map()
|
|
693
|
+
byId: new Map(),
|
|
694
|
+
completeRootKey: ""
|
|
534
695
|
};
|
|
535
696
|
|
|
536
697
|
function hydrateSessionMetadata(file, metadata) {
|
|
@@ -543,12 +704,12 @@ function hydrateSessionMetadata(file, metadata) {
|
|
|
543
704
|
return { ...metadata, conversationPath, metadataPath: file, transcriptPath, eventPath, viewPath, rawPath };
|
|
544
705
|
}
|
|
545
706
|
|
|
546
|
-
// TTL cache for the archive
|
|
547
|
-
// list-derived endpoints
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
const SNAPSHOT_CACHE_TTL_MS =
|
|
707
|
+
// TTL cache for the archive snapshot. The web viewer fires several
|
|
708
|
+
// list-derived endpoints while users move between history/stats/projects; keep
|
|
709
|
+
// the hydrated metadata snapshot warm long enough for that browsing session to
|
|
710
|
+
// feel instant. Import/write paths in this process invalidate explicitly via
|
|
711
|
+
// `invalidateSessionsSnapshotCache()`.
|
|
712
|
+
const SNAPSHOT_CACHE_TTL_MS = 15000;
|
|
552
713
|
const _snapshotCache = {
|
|
553
714
|
rootKey: "",
|
|
554
715
|
snapshot: null,
|
|
@@ -582,48 +743,35 @@ function invalidateSessionsSnapshotCache() {
|
|
|
582
743
|
_snapshotCache.takenAtMs = 0;
|
|
583
744
|
}
|
|
584
745
|
|
|
746
|
+
function listSessionsSnapshot(env = process.env) {
|
|
747
|
+
return readSessionListIndex(env) || rebuildSessionListIndex(env);
|
|
748
|
+
}
|
|
749
|
+
|
|
585
750
|
/**
|
|
586
751
|
* Walk the archive once and return both the hydrated session list and a
|
|
587
|
-
* fingerprint summarizing the snapshot.
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
* count changed (added/removed sessions).
|
|
591
|
-
*
|
|
592
|
-
* The fingerprint deliberately summarizes via XOR over per-file mtime/size
|
|
593
|
-
* values rather than concatenating sorted file paths — XOR is order-independent
|
|
594
|
-
* and folds the whole archive into a single 16-byte hex string in O(N) time
|
|
595
|
-
* with no allocation.
|
|
752
|
+
* fingerprint summarizing the snapshot. The result is persisted as a derived
|
|
753
|
+
* session-list index so steady-state web reads can avoid using the filesystem
|
|
754
|
+
* as a query engine.
|
|
596
755
|
*/
|
|
597
|
-
function
|
|
756
|
+
function rebuildSessionListIndex(env = process.env) {
|
|
598
757
|
const root = path.join(archiveRoot(env), "sessions");
|
|
599
758
|
const seen = new Set();
|
|
600
|
-
const
|
|
601
|
-
let count = 0;
|
|
602
|
-
// 64-bit XOR accumulators kept as JS BigInt so we never overflow. Slightly
|
|
603
|
-
// slower than Number arithmetic but the cost is dwarfed by fs.statSync.
|
|
604
|
-
let mtimeXor = 0n;
|
|
605
|
-
let sizeXor = 0n;
|
|
606
|
-
let maxMtimeMs = 0;
|
|
759
|
+
const entries = [];
|
|
607
760
|
walk(root, (file) => {
|
|
608
761
|
if (!file.endsWith(".metadata.json")) return;
|
|
609
762
|
seen.add(file);
|
|
610
763
|
let stat;
|
|
611
764
|
try { stat = fs.statSync(file); } catch { return; }
|
|
612
|
-
count += 1;
|
|
613
|
-
mtimeXor ^= BigInt(Math.trunc(stat.mtimeMs));
|
|
614
|
-
sizeXor ^= BigInt(stat.size);
|
|
615
|
-
if (stat.mtimeMs > maxMtimeMs) maxMtimeMs = stat.mtimeMs;
|
|
616
765
|
const cached = _sessionsIndex.byPath.get(file);
|
|
617
766
|
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
|
|
618
|
-
|
|
767
|
+
entries.push(sessionListEntry(cached.session, file, stat));
|
|
619
768
|
return;
|
|
620
769
|
}
|
|
621
770
|
const metadata = readJson(file, null);
|
|
622
771
|
if (!metadata) return;
|
|
623
772
|
const session = hydrateSessionMetadata(file, metadata);
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
sessions.push(session);
|
|
773
|
+
rememberSessionIndexEntry(sessionListEntry(session, file, stat));
|
|
774
|
+
entries.push(sessionListEntry(session, file, stat));
|
|
627
775
|
});
|
|
628
776
|
for (const key of [..._sessionsIndex.byPath.keys()]) {
|
|
629
777
|
if (!seen.has(key)) {
|
|
@@ -632,9 +780,341 @@ function listSessionsSnapshot(env = process.env) {
|
|
|
632
780
|
_sessionsIndex.byPath.delete(key);
|
|
633
781
|
}
|
|
634
782
|
}
|
|
635
|
-
|
|
636
|
-
const
|
|
637
|
-
|
|
783
|
+
_sessionsIndex.completeRootKey = root;
|
|
784
|
+
const snapshot = snapshotFromSessionListEntries(entries);
|
|
785
|
+
writeSessionListIndex(snapshot, env);
|
|
786
|
+
return snapshot;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function readSessionListIndex(env = process.env) {
|
|
790
|
+
const root = archiveRoot(env);
|
|
791
|
+
// Fast path: SQLite store. Indexed reads, sub-millisecond. Stat-validate
|
|
792
|
+
// each row against the on-disk metadata.json file — if anything drifted
|
|
793
|
+
// (mtime/size differ, or the file is gone) we treat the cache as stale
|
|
794
|
+
// and let the caller rebuild from a filesystem walk. The stat-check is
|
|
795
|
+
// bounded I/O (~50-80ms for 4k files) and only runs on cold reads outside
|
|
796
|
+
// the snapshot TTL.
|
|
797
|
+
const fromSqlite = sessionStore.readAllSessions(root);
|
|
798
|
+
if (fromSqlite && fromSqlite.length) {
|
|
799
|
+
const entries = fromSqlite.map(normalizeSessionListEntry).filter(Boolean);
|
|
800
|
+
if (entries.length && sessionListEntriesMatchDisk(entries) && sessionListEntriesCoverManifest(entries, root)) {
|
|
801
|
+
const fingerprint = sessionListFingerprint(entries);
|
|
802
|
+
replaceSessionMemoryIndex(entries, path.join(root, "sessions"));
|
|
803
|
+
return snapshotFromSessionListEntries(entries, { fingerprint, fromIndex: true });
|
|
804
|
+
}
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
// One-time migration: legacy JSON index. If present and valid, hydrate the
|
|
808
|
+
// SQLite store from it so subsequent reads take the fast path. The JSON
|
|
809
|
+
// file is left in place but no longer written.
|
|
810
|
+
const file = sessionListIndexPath(env);
|
|
811
|
+
let payload = null;
|
|
812
|
+
try {
|
|
813
|
+
payload = readJson(file, null);
|
|
814
|
+
} catch {
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
if (!payload || payload.version !== SESSION_LIST_INDEX_VERSION || payload.archiveRoot !== root) return null;
|
|
818
|
+
const entries = Array.isArray(payload.entries) ? payload.entries.map(normalizeSessionListEntry).filter(Boolean) : [];
|
|
819
|
+
if (entries.length !== Number(payload.count || 0)) return null;
|
|
820
|
+
const fingerprint = sessionListFingerprint(entries);
|
|
821
|
+
if (payload.fingerprint && payload.fingerprint !== fingerprint) return null;
|
|
822
|
+
if (!sessionListEntriesMatchDisk(entries) || !sessionListEntriesCoverManifest(entries, root)) return null;
|
|
823
|
+
// Best-effort migrate; if SQLite is unavailable we still serve from memory.
|
|
824
|
+
sessionStore.replaceAllSessions(entries, root);
|
|
825
|
+
replaceSessionMemoryIndex(entries, path.join(root, "sessions"));
|
|
826
|
+
return snapshotFromSessionListEntries(entries, {
|
|
827
|
+
fingerprint,
|
|
828
|
+
generatedAt: payload.generatedAt || "",
|
|
829
|
+
fromIndex: true
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function writeSessionListIndex(snapshot, env = process.env) {
|
|
834
|
+
const entries = Array.isArray(snapshot?.entries) ? snapshot.entries.map(normalizeSessionListEntry).filter(Boolean) : [];
|
|
835
|
+
const next = snapshotFromSessionListEntries(entries);
|
|
836
|
+
const root = archiveRoot(env);
|
|
837
|
+
// Bulk replace in the SQLite store. Used after filesystem rebuild.
|
|
838
|
+
if (sessionStore.replaceAllSessions(next.entries, root)) return true;
|
|
839
|
+
// Fallback: write the legacy JSON file so a setup without better-sqlite3
|
|
840
|
+
// still has a persistent session index.
|
|
841
|
+
try {
|
|
842
|
+
writeJson(sessionListIndexPath(env), {
|
|
843
|
+
version: SESSION_LIST_INDEX_VERSION,
|
|
844
|
+
archiveRoot: root,
|
|
845
|
+
generatedAt: new Date().toISOString(),
|
|
846
|
+
fingerprint: next.fingerprint,
|
|
847
|
+
count: next.count,
|
|
848
|
+
maxMtimeMs: next.maxMtimeMs,
|
|
849
|
+
entries: next.entries
|
|
850
|
+
}, { pretty: false });
|
|
851
|
+
return true;
|
|
852
|
+
} catch {
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function snapshotFromSessionListEntries(entries, options = {}) {
|
|
858
|
+
const normalized = (Array.isArray(entries) ? entries : []).map(normalizeSessionListEntry).filter(Boolean);
|
|
859
|
+
normalized.sort((a, b) => String(b.session.startedAt).localeCompare(String(a.session.startedAt)));
|
|
860
|
+
const sessions = normalized.map((entry) => entry.session);
|
|
861
|
+
return {
|
|
862
|
+
sessions,
|
|
863
|
+
entries: normalized,
|
|
864
|
+
fingerprint: options.fingerprint || sessionListFingerprint(normalized),
|
|
865
|
+
count: normalized.length,
|
|
866
|
+
maxMtimeMs: normalized.reduce((max, entry) => Math.max(max, entry.mtimeMs || 0), 0),
|
|
867
|
+
generatedAt: options.generatedAt || "",
|
|
868
|
+
fromIndex: Boolean(options.fromIndex)
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Returns true iff every entry's stored (mtime, size) still matches the
|
|
874
|
+
* current on-disk metadata.json. Out-of-band file edits (manual changes,
|
|
875
|
+
* file restores from backup) invalidate via mtime/size drift. Missing
|
|
876
|
+
* files also invalidate (the rebuild path will drop them).
|
|
877
|
+
*
|
|
878
|
+
* Intentionally returns false on the first mismatch — the cache is only
|
|
879
|
+
* useful if it's wholly accurate; partial freshness still requires a walk.
|
|
880
|
+
*/
|
|
881
|
+
function sessionListEntriesMatchDisk(entries) {
|
|
882
|
+
for (const entry of entries) {
|
|
883
|
+
if (!entry?.metadataPath) return false;
|
|
884
|
+
const stat = safeStat(entry.metadataPath);
|
|
885
|
+
if (!stat) return false;
|
|
886
|
+
if (Math.trunc(stat.mtimeMs) !== Math.trunc(entry.mtimeMs)) return false;
|
|
887
|
+
if (Number(stat.size) !== Number(entry.size)) return false;
|
|
888
|
+
}
|
|
889
|
+
return true;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function sessionListEntriesCoverManifest(entries, root) {
|
|
893
|
+
const manifest = readJson(path.join(root, "sessions", "manifest.json"), null);
|
|
894
|
+
if (!manifest || !Array.isArray(manifest.sessions) || !manifest.sessions.length) return true;
|
|
895
|
+
|
|
896
|
+
const indexedPaths = new Set(
|
|
897
|
+
entries
|
|
898
|
+
.map((entry) => entry?.metadataPath || "")
|
|
899
|
+
.filter(Boolean)
|
|
900
|
+
.map((file) => path.resolve(file))
|
|
901
|
+
);
|
|
902
|
+
let liveManifestPathCount = 0;
|
|
903
|
+
for (const item of manifest.sessions) {
|
|
904
|
+
const metadataPath = item?.metadataPath || "";
|
|
905
|
+
if (!metadataPath || !safeStat(metadataPath)) continue;
|
|
906
|
+
liveManifestPathCount += 1;
|
|
907
|
+
if (!indexedPaths.has(path.resolve(metadataPath))) return false;
|
|
908
|
+
}
|
|
909
|
+
return liveManifestPathCount <= entries.length;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function sessionListFingerprint(entries) {
|
|
913
|
+
const rows = (Array.isArray(entries) ? entries : []).map(normalizeSessionListEntry).filter(Boolean);
|
|
914
|
+
if (!rows.length) return "0-0-0";
|
|
915
|
+
let mtimeXor = 0n;
|
|
916
|
+
let sizeXor = 0n;
|
|
917
|
+
const hash = crypto.createHash("sha1");
|
|
918
|
+
for (const entry of [...rows].sort((a, b) => a.metadataPath.localeCompare(b.metadataPath))) {
|
|
919
|
+
const mtime = Math.trunc(entry.mtimeMs || 0);
|
|
920
|
+
const size = Math.trunc(entry.size || 0);
|
|
921
|
+
mtimeXor ^= BigInt(mtime);
|
|
922
|
+
sizeXor ^= BigInt(size);
|
|
923
|
+
hash.update(entry.metadataPath).update("\0").update(String(mtime)).update("\0").update(String(size)).update("\0");
|
|
924
|
+
}
|
|
925
|
+
return `${rows.length.toString(36)}-${mtimeXor.toString(16)}-${sizeXor.toString(16)}-${hash.digest("base64url").slice(0, 16)}`;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function sessionListEntry(session, metadataPath, stat) {
|
|
929
|
+
return normalizeSessionListEntry({
|
|
930
|
+
metadataPath,
|
|
931
|
+
mtimeMs: stat?.mtimeMs || 0,
|
|
932
|
+
size: stat?.size || 0,
|
|
933
|
+
session
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function normalizeSessionListEntry(entry) {
|
|
938
|
+
if (!entry || typeof entry !== "object" || !entry.session || typeof entry.session !== "object") return null;
|
|
939
|
+
const metadataPath = entry.metadataPath || entry.session.metadataPath || "";
|
|
940
|
+
if (!metadataPath) return null;
|
|
941
|
+
const session = hydrateSessionListIndexMetadata(metadataPath, entry.session);
|
|
942
|
+
return {
|
|
943
|
+
metadataPath,
|
|
944
|
+
mtimeMs: Number(entry.mtimeMs || 0),
|
|
945
|
+
size: Number(entry.size || 0),
|
|
946
|
+
session
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function hydrateSessionListIndexMetadata(file, metadata) {
|
|
951
|
+
return {
|
|
952
|
+
...metadata,
|
|
953
|
+
conversationPath: metadata.conversationPath || file.replace(/\.metadata\.json$/, ".conversation.md"),
|
|
954
|
+
metadataPath: file,
|
|
955
|
+
transcriptPath: metadata.transcriptPath || file.replace(/\.metadata\.json$/, ".transcript.jsonl"),
|
|
956
|
+
eventPath: metadata.eventPath || file.replace(/\.metadata\.json$/, ".events.jsonl"),
|
|
957
|
+
viewPath: metadata.viewPath || file.replace(/\.metadata\.json$/, ".view.json"),
|
|
958
|
+
rawPath: metadata.rawPath || ""
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function rememberSessionIndexEntry(entry) {
|
|
963
|
+
const normalized = normalizeSessionListEntry(entry);
|
|
964
|
+
if (!normalized) return;
|
|
965
|
+
_sessionsIndex.byPath.set(normalized.metadataPath, {
|
|
966
|
+
mtimeMs: normalized.mtimeMs,
|
|
967
|
+
size: normalized.size,
|
|
968
|
+
session: normalized.session
|
|
969
|
+
});
|
|
970
|
+
if (normalized.session.sessionId) _sessionsIndex.byId.set(normalized.session.sessionId, normalized.metadataPath);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function replaceSessionMemoryIndex(entries, rootKey = "") {
|
|
974
|
+
_sessionsIndex.byPath.clear();
|
|
975
|
+
_sessionsIndex.byId.clear();
|
|
976
|
+
_sessionsIndex.completeRootKey = rootKey;
|
|
977
|
+
for (const entry of entries) rememberSessionIndexEntry(entry);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function forgetSessionIndexPath(metadataPath) {
|
|
981
|
+
if (!metadataPath) return;
|
|
982
|
+
const existing = _sessionsIndex.byPath.get(metadataPath);
|
|
983
|
+
if (existing?.session?.sessionId) _sessionsIndex.byId.delete(existing.session.sessionId);
|
|
984
|
+
_sessionsIndex.byPath.delete(metadataPath);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function updateSessionListIndexAfterWrite(session, metadataPath, env = process.env, options = {}) {
|
|
988
|
+
const stat = safeStat(metadataPath);
|
|
989
|
+
if (!stat) return;
|
|
990
|
+
const sourcePath = session.sourcePath || "";
|
|
991
|
+
const replaceSourcePathCopies = options.replaceSourcePathCopies !== false;
|
|
992
|
+
const resolvedMetadataPath = path.resolve(metadataPath);
|
|
993
|
+
const supersededSessionIds = new Set();
|
|
994
|
+
const supersededMetadataPaths = new Set();
|
|
995
|
+
for (const [file, cached] of [..._sessionsIndex.byPath.entries()]) {
|
|
996
|
+
const existing = cached?.session || {};
|
|
997
|
+
if (path.resolve(file) === resolvedMetadataPath) {
|
|
998
|
+
forgetSessionIndexPath(file);
|
|
999
|
+
// When the same metadata file is being rewritten under a *different*
|
|
1000
|
+
// session id (e.g. a re-import that recomputed stableSessionId after
|
|
1001
|
+
// a parser change), the prior row keyed on existing.sessionId would
|
|
1002
|
+
// otherwise stay orphaned in SQLite — the upsert below inserts under
|
|
1003
|
+
// the new id and the per-metadata-path delete is skipped because the
|
|
1004
|
+
// path matches the incoming entry. Capture the old session id here
|
|
1005
|
+
// so the SQLite delete loop evicts it explicitly.
|
|
1006
|
+
if (existing.sessionId && existing.sessionId !== session.sessionId) {
|
|
1007
|
+
supersededSessionIds.add(existing.sessionId);
|
|
1008
|
+
}
|
|
1009
|
+
supersededMetadataPaths.add(file);
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
if (existing.sessionId && existing.sessionId === session.sessionId) {
|
|
1013
|
+
forgetSessionIndexPath(file);
|
|
1014
|
+
if (existing.sessionId) supersededSessionIds.add(existing.sessionId);
|
|
1015
|
+
supersededMetadataPaths.add(file);
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
if (replaceSourcePathCopies && sourcePath && existing.provider === session.provider && existing.sourcePath === sourcePath) {
|
|
1019
|
+
forgetSessionIndexPath(file);
|
|
1020
|
+
if (existing.sessionId) supersededSessionIds.add(existing.sessionId);
|
|
1021
|
+
supersededMetadataPaths.add(file);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
const nextEntry = sessionListEntry(session, metadataPath, stat);
|
|
1025
|
+
rememberSessionIndexEntry(nextEntry);
|
|
1026
|
+
// Fast path: incremental SQLite upsert + targeted deletes for superseded
|
|
1027
|
+
// rows. Avoids reading the whole index just to add one row. Includes a
|
|
1028
|
+
// SQL-level delete for (provider, sourcePath) since the in-memory loop
|
|
1029
|
+
// above only knows about entries currently cached in this process — rows
|
|
1030
|
+
// from prior process lifetimes still live in the SQLite store and would
|
|
1031
|
+
// otherwise survive.
|
|
1032
|
+
const root = archiveRoot(env);
|
|
1033
|
+
if (sessionStore.storeAvailable(root)) {
|
|
1034
|
+
for (const id of supersededSessionIds) {
|
|
1035
|
+
if (id && id !== session.sessionId) sessionStore.deleteSession({ sessionId: id }, root);
|
|
1036
|
+
}
|
|
1037
|
+
for (const file of supersededMetadataPaths) {
|
|
1038
|
+
if (file && path.resolve(file) !== resolvedMetadataPath) sessionStore.deleteSession({ metadataPath: file }, root);
|
|
1039
|
+
}
|
|
1040
|
+
if (replaceSourcePathCopies && sourcePath) {
|
|
1041
|
+
sessionStore.deleteSameSourceSessions({
|
|
1042
|
+
provider: session.provider || "",
|
|
1043
|
+
sourcePath,
|
|
1044
|
+
keepSessionId: session.sessionId || ""
|
|
1045
|
+
}, root);
|
|
1046
|
+
}
|
|
1047
|
+
sessionStore.upsertSession(nextEntry, root);
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
// Legacy JSON fallback for setups without better-sqlite3.
|
|
1051
|
+
const indexPath = sessionListIndexPath(env);
|
|
1052
|
+
if (!fs.existsSync(indexPath)) return;
|
|
1053
|
+
let payload = null;
|
|
1054
|
+
try {
|
|
1055
|
+
payload = readJson(indexPath, null);
|
|
1056
|
+
} catch {
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
if (
|
|
1060
|
+
!payload ||
|
|
1061
|
+
payload.version !== SESSION_LIST_INDEX_VERSION ||
|
|
1062
|
+
payload.archiveRoot !== archiveRoot(env) ||
|
|
1063
|
+
!Array.isArray(payload.entries)
|
|
1064
|
+
) {
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
const entries = payload.entries
|
|
1068
|
+
.map(normalizeSessionListEntry)
|
|
1069
|
+
.filter(Boolean)
|
|
1070
|
+
.filter((entry) => {
|
|
1071
|
+
const existing = entry.session || {};
|
|
1072
|
+
if (path.resolve(entry.metadataPath) === resolvedMetadataPath) return false;
|
|
1073
|
+
if (existing.sessionId && existing.sessionId === session.sessionId) return false;
|
|
1074
|
+
if (replaceSourcePathCopies && sourcePath && existing.provider === session.provider && existing.sourcePath === sourcePath) return false;
|
|
1075
|
+
return true;
|
|
1076
|
+
});
|
|
1077
|
+
entries.push(nextEntry);
|
|
1078
|
+
writeSessionListIndex({ entries }, env);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function removeSessionFromListIndex(session, env = process.env) {
|
|
1082
|
+
const root = archiveRoot(env);
|
|
1083
|
+
// Fast path: targeted SQLite delete.
|
|
1084
|
+
if (sessionStore.storeAvailable(root)) {
|
|
1085
|
+
sessionStore.deleteSession({
|
|
1086
|
+
sessionId: session.sessionId || "",
|
|
1087
|
+
metadataPath: session.metadataPath || ""
|
|
1088
|
+
}, root);
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
const indexPath = sessionListIndexPath(env);
|
|
1092
|
+
if (!fs.existsSync(indexPath)) return;
|
|
1093
|
+
let payload = null;
|
|
1094
|
+
try {
|
|
1095
|
+
payload = readJson(indexPath, null);
|
|
1096
|
+
} catch {
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
if (
|
|
1100
|
+
!payload ||
|
|
1101
|
+
payload.version !== SESSION_LIST_INDEX_VERSION ||
|
|
1102
|
+
payload.archiveRoot !== archiveRoot(env) ||
|
|
1103
|
+
!Array.isArray(payload.entries)
|
|
1104
|
+
) {
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
const metadataPath = session.metadataPath || "";
|
|
1108
|
+
const resolvedMetadataPath = metadataPath ? path.resolve(metadataPath) : "";
|
|
1109
|
+
const entries = payload.entries
|
|
1110
|
+
.map(normalizeSessionListEntry)
|
|
1111
|
+
.filter(Boolean)
|
|
1112
|
+
.filter((entry) => {
|
|
1113
|
+
if (resolvedMetadataPath && path.resolve(entry.metadataPath) === resolvedMetadataPath) return false;
|
|
1114
|
+
if (session.sessionId && entry.session.sessionId === session.sessionId) return false;
|
|
1115
|
+
return true;
|
|
1116
|
+
});
|
|
1117
|
+
writeSessionListIndex({ entries }, env);
|
|
638
1118
|
}
|
|
639
1119
|
|
|
640
1120
|
function findSessionById(sessionId, env = process.env) {
|
|
@@ -668,10 +1148,13 @@ function computeSessionUsage(messages) {
|
|
|
668
1148
|
let inputTokens = 0;
|
|
669
1149
|
let outputTokens = 0;
|
|
670
1150
|
let cacheInputTokens = 0;
|
|
1151
|
+
let cacheCreationInputTokens = 0;
|
|
1152
|
+
let cacheReadInputTokens = 0;
|
|
671
1153
|
let countedCacheInputTokens = 0;
|
|
672
1154
|
let reasoningOutputTokens = 0;
|
|
673
1155
|
let countedReasoningOutputTokens = 0;
|
|
674
1156
|
let toolUsePromptTokens = 0;
|
|
1157
|
+
let costUsd = 0;
|
|
675
1158
|
let maxUndifferentiatedTotalTokens = 0;
|
|
676
1159
|
let totalInputTokens = 0;
|
|
677
1160
|
let totalOutputTokens = 0;
|
|
@@ -705,10 +1188,24 @@ function computeSessionUsage(messages) {
|
|
|
705
1188
|
any = true;
|
|
706
1189
|
}
|
|
707
1190
|
if (summary.cacheInputTokens > 0) cacheInputTokens += summary.cacheInputTokens;
|
|
1191
|
+
if (summary.cacheCreationInputTokens > 0) cacheCreationInputTokens += summary.cacheCreationInputTokens;
|
|
1192
|
+
if (summary.cacheReadInputTokens > 0) cacheReadInputTokens += summary.cacheReadInputTokens;
|
|
708
1193
|
if (summary.countedCacheInputTokens > 0) countedCacheInputTokens += summary.countedCacheInputTokens;
|
|
709
1194
|
if (summary.reasoningOutputTokens > 0) reasoningOutputTokens += summary.reasoningOutputTokens;
|
|
710
1195
|
if (summary.countedReasoningOutputTokens > 0) countedReasoningOutputTokens += summary.countedReasoningOutputTokens;
|
|
711
1196
|
if (summary.toolUsePromptTokens > 0) toolUsePromptTokens += summary.toolUsePromptTokens;
|
|
1197
|
+
const messageCostUsd = positiveCostNumber(firstUsageNumber(
|
|
1198
|
+
usage.costUsd,
|
|
1199
|
+
usage.costUSD,
|
|
1200
|
+
usage.cost_usd,
|
|
1201
|
+
usage.totalCostUsd,
|
|
1202
|
+
usage.total_cost_usd,
|
|
1203
|
+
message.metadata?.costUsd,
|
|
1204
|
+
message.metadata?.costUSD,
|
|
1205
|
+
message.metadata?.cost_usd,
|
|
1206
|
+
message.metadata?.cost
|
|
1207
|
+
));
|
|
1208
|
+
if (messageCostUsd > 0) costUsd += messageCostUsd;
|
|
712
1209
|
if (!summary.hasSplitTokens && !summary.hasExtraTokens && summary.hasTotalTokens) {
|
|
713
1210
|
maxUndifferentiatedTotalTokens = Math.max(maxUndifferentiatedTotalTokens, summary.totalTokens);
|
|
714
1211
|
}
|
|
@@ -736,15 +1233,411 @@ function computeSessionUsage(messages) {
|
|
|
736
1233
|
totalInputTokens: totalInputTokens || inputTokens,
|
|
737
1234
|
totalOutputTokens: totalOutputTokens || outputTokens
|
|
738
1235
|
};
|
|
1236
|
+
if (cacheCreationInputTokens > 0) result.cacheCreationInputTokens = cacheCreationInputTokens;
|
|
1237
|
+
if (cacheReadInputTokens > 0) result.cacheReadInputTokens = cacheReadInputTokens;
|
|
739
1238
|
if (reasoningOutputTokens > 0 && countedReasoningOutputTokens === 0) result.reasoningOutputTokensIncludedInOutput = true;
|
|
740
1239
|
if (cacheInputTokens > 0 && countedCacheInputTokens === 0) result.cacheInputTokensIncludedInInput = true;
|
|
741
1240
|
if (estimatedUsageCount > 0 && actualUsageCount === 0) {
|
|
742
1241
|
result.estimated = true;
|
|
743
1242
|
if (estimationMethods.size === 1) result.estimationMethod = Array.from(estimationMethods)[0];
|
|
744
1243
|
}
|
|
1244
|
+
if (costUsd > 0) {
|
|
1245
|
+
result.costUsd = Number(costUsd.toFixed(8));
|
|
1246
|
+
result.currency = "USD";
|
|
1247
|
+
result.costSource = "provider-usage";
|
|
1248
|
+
result.costEstimated = false;
|
|
1249
|
+
}
|
|
745
1250
|
return result;
|
|
746
1251
|
}
|
|
747
1252
|
|
|
1253
|
+
function computeOutputTokenWork(messages) {
|
|
1254
|
+
let textTokens = 0;
|
|
1255
|
+
let toolUseTokens = 0;
|
|
1256
|
+
let reasoningTokens = 0;
|
|
1257
|
+
let unknownTokens = 0;
|
|
1258
|
+
let messageCount = 0;
|
|
1259
|
+
let mixedMessageCount = 0;
|
|
1260
|
+
let estimatedUsageCount = 0;
|
|
1261
|
+
let actualUsageCount = 0;
|
|
1262
|
+
const seenUsageRequests = new Set();
|
|
1263
|
+
for (const message of messages || []) {
|
|
1264
|
+
if (String(message?.role || "").toLowerCase() !== "assistant") continue;
|
|
1265
|
+
const usage = message?.metadata?.usage;
|
|
1266
|
+
if (!usage || typeof usage !== "object") continue;
|
|
1267
|
+
const requestId = String(message.metadata?.requestId || "").trim();
|
|
1268
|
+
if (requestId) {
|
|
1269
|
+
if (seenUsageRequests.has(requestId)) continue;
|
|
1270
|
+
seenUsageRequests.add(requestId);
|
|
1271
|
+
}
|
|
1272
|
+
const summary = usageTokenSummary(usage);
|
|
1273
|
+
if (!summary.hasAny) continue;
|
|
1274
|
+
const output = positiveTokenNumber(summary.outputTokens);
|
|
1275
|
+
const reasoning = positiveTokenNumber(summary.reasoningOutputTokens);
|
|
1276
|
+
if (output <= 0 && reasoning <= 0) continue;
|
|
1277
|
+
messageCount++;
|
|
1278
|
+
if (summary.estimated) estimatedUsageCount++;
|
|
1279
|
+
else actualUsageCount++;
|
|
1280
|
+
|
|
1281
|
+
if (reasoning > 0) reasoningTokens += reasoning;
|
|
1282
|
+
const nonReasoningOutput = Math.max(0, output - (summary.reasoningOutputTokensIncludedInOutput ? reasoning : 0));
|
|
1283
|
+
if (nonReasoningOutput <= 0) continue;
|
|
1284
|
+
const kind = outputTokenWorkKind(message);
|
|
1285
|
+
if (kind === "text") textTokens += nonReasoningOutput;
|
|
1286
|
+
else if (kind === "toolUse") toolUseTokens += nonReasoningOutput;
|
|
1287
|
+
else if (kind === "reasoning") reasoningTokens += nonReasoningOutput;
|
|
1288
|
+
else {
|
|
1289
|
+
unknownTokens += nonReasoningOutput;
|
|
1290
|
+
if (kind === "mixed") mixedMessageCount++;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
const totalTokens = textTokens + toolUseTokens + reasoningTokens + unknownTokens;
|
|
1294
|
+
if (!totalTokens) return null;
|
|
1295
|
+
const knownTokens = textTokens + toolUseTokens + reasoningTokens;
|
|
1296
|
+
const result = {
|
|
1297
|
+
version: OUTPUT_TOKEN_WORK_VERSION,
|
|
1298
|
+
textTokens,
|
|
1299
|
+
toolUseTokens,
|
|
1300
|
+
reasoningTokens,
|
|
1301
|
+
unknownTokens,
|
|
1302
|
+
totalTokens,
|
|
1303
|
+
knownTokens,
|
|
1304
|
+
coverage: Number((knownTokens / totalTokens).toFixed(4)),
|
|
1305
|
+
messageCount
|
|
1306
|
+
};
|
|
1307
|
+
if (mixedMessageCount) result.mixedMessageCount = mixedMessageCount;
|
|
1308
|
+
if (estimatedUsageCount > 0 && actualUsageCount === 0) result.estimated = true;
|
|
1309
|
+
return result;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function outputTokenWorkKind(message) {
|
|
1313
|
+
const metadata = message?.metadata || {};
|
|
1314
|
+
const summaryKind = String(metadata.summaryKind || metadata.summary_kind || "").toLowerCase();
|
|
1315
|
+
if (metadata.supplementary && /^(thinking|reasoning)$/.test(summaryKind)) return "reasoning";
|
|
1316
|
+
if (metadata.supplementary) return "unknown";
|
|
1317
|
+
const hasText = String(message?.content || "").trim().length > 0;
|
|
1318
|
+
const hasTools = Array.isArray(metadata.toolCalls) && metadata.toolCalls.length > 0;
|
|
1319
|
+
if (hasText && hasTools) return "mixed";
|
|
1320
|
+
if (hasTools) return "toolUse";
|
|
1321
|
+
if (hasText) return "text";
|
|
1322
|
+
return "unknown";
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function collectSessionOutcomes(events) {
|
|
1326
|
+
let editToolCalls = 0;
|
|
1327
|
+
let knowledgeCaptures = 0;
|
|
1328
|
+
let memoryReads = 0;
|
|
1329
|
+
let memoryWrites = 0;
|
|
1330
|
+
let memoryLoads = 0;
|
|
1331
|
+
const touchedFiles = new Set();
|
|
1332
|
+
const editCallsByEventId = new Map();
|
|
1333
|
+
for (const event of Array.isArray(events) ? events : []) {
|
|
1334
|
+
const kind = String(event?.kind || "");
|
|
1335
|
+
if (kind === "memory.read") {
|
|
1336
|
+
memoryReads++;
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
if (kind === "memory.write") {
|
|
1340
|
+
memoryWrites++;
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
if (kind === "memory.loaded") {
|
|
1344
|
+
memoryLoads++;
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
if (kind === "tool.called") {
|
|
1348
|
+
const call = event?.body?.toolCall || {};
|
|
1349
|
+
const indexed = event?.indexed || {};
|
|
1350
|
+
const rawName = firstStringValue(indexed.toolName, call.name, call.displayName, indexed.title, "tool");
|
|
1351
|
+
const category = normalizeToolUsageCategory(firstStringValue(indexed.toolCategory, call.category, call.rawCategory), rawName);
|
|
1352
|
+
if (category !== "edit") continue;
|
|
1353
|
+
editToolCalls++;
|
|
1354
|
+
const paths = collectToolCallPathCandidates(call, indexed);
|
|
1355
|
+
let capturedKnowledge = false;
|
|
1356
|
+
for (const candidate of paths) {
|
|
1357
|
+
const normalized = normalizeOutcomePath(candidate);
|
|
1358
|
+
if (!normalized) continue;
|
|
1359
|
+
touchedFiles.add(normalized);
|
|
1360
|
+
if (isKnowledgeCapturePath(normalized)) capturedKnowledge = true;
|
|
1361
|
+
}
|
|
1362
|
+
if (event.eventId) editCallsByEventId.set(event.eventId, { capturedKnowledge });
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
if (kind === "tool.completed") {
|
|
1366
|
+
const parentEventId = String(event.parentEventId || "");
|
|
1367
|
+
const pending = parentEventId ? editCallsByEventId.get(parentEventId) : null;
|
|
1368
|
+
if (!pending || !pending.capturedKnowledge) continue;
|
|
1369
|
+
const status = String(event?.indexed?.status || event?.body?.toolResult?.status || "").toLowerCase();
|
|
1370
|
+
if (status.includes("error") || status.includes("fail")) continue;
|
|
1371
|
+
knowledgeCaptures++;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
return {
|
|
1375
|
+
version: SESSION_OUTCOMES_VERSION,
|
|
1376
|
+
editToolCalls,
|
|
1377
|
+
filesTouched: touchedFiles.size,
|
|
1378
|
+
knowledgeCaptures,
|
|
1379
|
+
memoryReads,
|
|
1380
|
+
memoryWrites,
|
|
1381
|
+
memoryLoads
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
const OUTCOME_PATH_ARGUMENT_KEYS = new Set([
|
|
1386
|
+
"path",
|
|
1387
|
+
"paths",
|
|
1388
|
+
"file",
|
|
1389
|
+
"files",
|
|
1390
|
+
"filename",
|
|
1391
|
+
"filenames",
|
|
1392
|
+
"file_path",
|
|
1393
|
+
"filepath",
|
|
1394
|
+
"relative_path",
|
|
1395
|
+
"relativeworkspacepath",
|
|
1396
|
+
"relative_workspace_path",
|
|
1397
|
+
"target",
|
|
1398
|
+
"target_path",
|
|
1399
|
+
"source_path",
|
|
1400
|
+
"destination_path"
|
|
1401
|
+
]);
|
|
1402
|
+
|
|
1403
|
+
function collectToolCallPathCandidates(call, indexed = {}) {
|
|
1404
|
+
const paths = [];
|
|
1405
|
+
appendOutcomePath(paths, call?.target);
|
|
1406
|
+
appendOutcomePath(paths, indexed?.target);
|
|
1407
|
+
collectPathValues(call?.arguments, "", 0, paths);
|
|
1408
|
+
collectPathValues(call?.input, "", 0, paths);
|
|
1409
|
+
return Array.from(new Set(paths.map(normalizeOutcomePath).filter(Boolean)));
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function collectPathValues(value, key = "", depth = 0, out = []) {
|
|
1413
|
+
if (value == null || depth > 5) return out;
|
|
1414
|
+
if (typeof value === "string") {
|
|
1415
|
+
if (isOutcomePathKey(key)) appendOutcomePath(out, value);
|
|
1416
|
+
return out;
|
|
1417
|
+
}
|
|
1418
|
+
if (Array.isArray(value)) {
|
|
1419
|
+
for (const item of value) collectPathValues(item, key, depth + 1, out);
|
|
1420
|
+
return out;
|
|
1421
|
+
}
|
|
1422
|
+
if (typeof value !== "object") return out;
|
|
1423
|
+
for (const [childKey, childValue] of Object.entries(value)) {
|
|
1424
|
+
collectPathValues(childValue, childKey, depth + 1, out);
|
|
1425
|
+
}
|
|
1426
|
+
return out;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function isOutcomePathKey(key) {
|
|
1430
|
+
const normalized = String(key || "").replace(/[^a-z0-9]+/gi, "_").replace(/^_+|_+$/g, "").toLowerCase();
|
|
1431
|
+
return OUTCOME_PATH_ARGUMENT_KEYS.has(normalized);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function appendOutcomePath(out, value) {
|
|
1435
|
+
if (typeof value !== "string") return;
|
|
1436
|
+
const normalized = normalizeOutcomePath(value);
|
|
1437
|
+
if (normalized) out.push(normalized);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
function normalizeOutcomePath(value) {
|
|
1441
|
+
const text = String(value || "").trim();
|
|
1442
|
+
if (!text || text.length > 4096) return "";
|
|
1443
|
+
return text.replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function isKnowledgeCapturePath(value) {
|
|
1447
|
+
const normalized = normalizeOutcomePath(value).toLowerCase();
|
|
1448
|
+
if (!normalized) return false;
|
|
1449
|
+
if (memoryPathInfo(normalized)) return true;
|
|
1450
|
+
const base = path.basename(normalized);
|
|
1451
|
+
if (["agents.md", "claude.md", "gemini.md", "skill.md", ".cursorrules", ".windsurfrules", "rules.md", "memory.md"].includes(base)) return true;
|
|
1452
|
+
if (/\/\.(agents|codex|claude)\/skills\//.test(normalized)) return true;
|
|
1453
|
+
if (/\/\.cursor\/rules\//.test(normalized) || /\/\.windsurf\/rules\//.test(normalized)) return true;
|
|
1454
|
+
if (/\/skills\/[^/]+\/skill\.md$/.test(normalized)) return true;
|
|
1455
|
+
if (/\b(adr|adrs|decision|decisions|plan|plans|planning|rfc|rfcs|roadmap|project-rules)\b/.test(normalized) && /\.(md|mdx|rst|txt|adoc|org)$/i.test(base)) return true;
|
|
1456
|
+
return false;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function collectSessionToolUsage(events) {
|
|
1460
|
+
const byTool = new Map();
|
|
1461
|
+
const byCategory = new Map();
|
|
1462
|
+
const bySkill = new Map();
|
|
1463
|
+
const byMcpServer = new Map();
|
|
1464
|
+
const skillByCallId = new Map();
|
|
1465
|
+
let callCount = 0;
|
|
1466
|
+
let completedCount = 0;
|
|
1467
|
+
let errorCount = 0;
|
|
1468
|
+
for (const event of Array.isArray(events) ? events : []) {
|
|
1469
|
+
const kind = String(event?.kind || "");
|
|
1470
|
+
if (kind !== "tool.called" && kind !== "tool.completed") continue;
|
|
1471
|
+
const call = event?.body?.toolCall || {};
|
|
1472
|
+
const result = event?.body?.toolResult || {};
|
|
1473
|
+
const indexed = event?.indexed || {};
|
|
1474
|
+
const rawName = firstStringValue(
|
|
1475
|
+
indexed.toolName,
|
|
1476
|
+
call.name,
|
|
1477
|
+
call.displayName,
|
|
1478
|
+
result.name,
|
|
1479
|
+
result.kind,
|
|
1480
|
+
indexed.title,
|
|
1481
|
+
"tool"
|
|
1482
|
+
);
|
|
1483
|
+
const toolName = normalizeToolUsageName(rawName);
|
|
1484
|
+
const toolLabel = firstStringValue(call.displayName, indexed.title, result.kind, rawName, toolName);
|
|
1485
|
+
const category = normalizeToolUsageCategory(firstStringValue(indexed.toolCategory, call.category, result.category, call.rawCategory, result.rawCategory), toolName);
|
|
1486
|
+
const status = String(indexed.status || call.status || result.status || "").toLowerCase();
|
|
1487
|
+
const callId = firstStringValue(call.id, result.id);
|
|
1488
|
+
const mcpServer = mcpServerFromToolUsage(toolName, category);
|
|
1489
|
+
if (kind === "tool.called") {
|
|
1490
|
+
callCount++;
|
|
1491
|
+
const tool = byTool.get(toolName) || { tool_name: toolName, tool_label: toolLabel, tool_category: category, calls: 0, completed: 0, errors: 0 };
|
|
1492
|
+
tool.calls += 1;
|
|
1493
|
+
tool.tool_label = tool.tool_label || toolLabel;
|
|
1494
|
+
tool.tool_category = tool.tool_category || category;
|
|
1495
|
+
byTool.set(toolName, tool);
|
|
1496
|
+
const categoryEntry = byCategory.get(category) || { tool_category: category, calls: 0, completed: 0, errors: 0 };
|
|
1497
|
+
categoryEntry.calls += 1;
|
|
1498
|
+
byCategory.set(category, categoryEntry);
|
|
1499
|
+
const skill = skillFromToolCall(toolName, call);
|
|
1500
|
+
if (skill) {
|
|
1501
|
+
if (callId) skillByCallId.set(callId, skill);
|
|
1502
|
+
const skillEntry = bySkill.get(skill.skill_name) || { skill_name: skill.skill_name, skill_label: skill.skill_label, calls: 0, completed: 0, errors: 0 };
|
|
1503
|
+
skillEntry.calls += 1;
|
|
1504
|
+
bySkill.set(skill.skill_name, skillEntry);
|
|
1505
|
+
}
|
|
1506
|
+
if (mcpServer) {
|
|
1507
|
+
const serverEntry = byMcpServer.get(mcpServer.server_name) || { server_name: mcpServer.server_name, server_label: mcpServer.server_label, calls: 0, completed: 0, errors: 0 };
|
|
1508
|
+
serverEntry.calls += 1;
|
|
1509
|
+
byMcpServer.set(mcpServer.server_name, serverEntry);
|
|
1510
|
+
}
|
|
1511
|
+
} else if (kind === "tool.completed") {
|
|
1512
|
+
completedCount++;
|
|
1513
|
+
const failed = status.includes("error") || status.includes("fail");
|
|
1514
|
+
if (failed) errorCount++;
|
|
1515
|
+
const tool = byTool.get(toolName) || { tool_name: toolName, tool_label: toolLabel, tool_category: category, calls: 0, completed: 0, errors: 0 };
|
|
1516
|
+
tool.completed += 1;
|
|
1517
|
+
if (failed) tool.errors += 1;
|
|
1518
|
+
byTool.set(toolName, tool);
|
|
1519
|
+
const categoryEntry = byCategory.get(category) || { tool_category: category, calls: 0, completed: 0, errors: 0 };
|
|
1520
|
+
categoryEntry.completed += 1;
|
|
1521
|
+
if (failed) categoryEntry.errors += 1;
|
|
1522
|
+
byCategory.set(category, categoryEntry);
|
|
1523
|
+
const skill = callId ? skillByCallId.get(callId) : null;
|
|
1524
|
+
if (skill) {
|
|
1525
|
+
const skillEntry = bySkill.get(skill.skill_name) || { skill_name: skill.skill_name, skill_label: skill.skill_label, calls: 0, completed: 0, errors: 0 };
|
|
1526
|
+
skillEntry.completed += 1;
|
|
1527
|
+
if (failed) skillEntry.errors += 1;
|
|
1528
|
+
bySkill.set(skill.skill_name, skillEntry);
|
|
1529
|
+
}
|
|
1530
|
+
if (mcpServer) {
|
|
1531
|
+
const serverEntry = byMcpServer.get(mcpServer.server_name) || { server_name: mcpServer.server_name, server_label: mcpServer.server_label, calls: 0, completed: 0, errors: 0 };
|
|
1532
|
+
serverEntry.completed += 1;
|
|
1533
|
+
if (failed) serverEntry.errors += 1;
|
|
1534
|
+
byMcpServer.set(mcpServer.server_name, serverEntry);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
return {
|
|
1539
|
+
callCount,
|
|
1540
|
+
completedCount,
|
|
1541
|
+
errorCount,
|
|
1542
|
+
byTool: Array.from(byTool.values()).sort((a, b) => b.calls - a.calls || String(a.tool_label).localeCompare(String(b.tool_label))),
|
|
1543
|
+
byCategory: Array.from(byCategory.values()).sort((a, b) => b.calls - a.calls || String(a.tool_category).localeCompare(String(b.tool_category))),
|
|
1544
|
+
bySkill: Array.from(bySkill.values()).sort((a, b) => b.calls - a.calls || String(a.skill_label).localeCompare(String(b.skill_label))),
|
|
1545
|
+
// Some providers only record MCP results (claude.ai connectors, older
|
|
1546
|
+
// Codex apps), so treat completions as a floor for call counts.
|
|
1547
|
+
byMcpServer: Array.from(byMcpServer.values())
|
|
1548
|
+
.map((entry) => ({ ...entry, calls: Math.max(entry.calls, entry.completed) }))
|
|
1549
|
+
.sort((a, b) => b.calls - a.calls || String(a.server_label).localeCompare(String(b.server_label)))
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function firstStringValue(...values) {
|
|
1554
|
+
for (const value of values) {
|
|
1555
|
+
if (typeof value !== "string") continue;
|
|
1556
|
+
const text = value.trim();
|
|
1557
|
+
if (text) return text;
|
|
1558
|
+
}
|
|
1559
|
+
return "";
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
function normalizeToolUsageName(value) {
|
|
1563
|
+
return String(value || "tool").trim().toLowerCase().replace(/[^a-z0-9._:-]+/g, "_").replace(/^_+|_+$/g, "") || "tool";
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
const SKILL_TOOL_NAMES = new Set(["skill", "load_skill", "read_skill", "slashcommand", "slash_command"]);
|
|
1567
|
+
// Matches skill loads done through file reads (Codex shells out to read
|
|
1568
|
+
// <skills root>/<name>/SKILL.md; other agents use Read-style tools).
|
|
1569
|
+
const SKILL_FILE_PATH_RE = /[\/\\]skills[\/\\]([A-Za-z0-9._-]+)[\/\\]SKILL\.md/i;
|
|
1570
|
+
|
|
1571
|
+
function skillFromToolCall(toolName, call) {
|
|
1572
|
+
if (SKILL_TOOL_NAMES.has(toolName)) {
|
|
1573
|
+
const args = call?.arguments && typeof call.arguments === "object" && !Array.isArray(call.arguments) ? call.arguments : {};
|
|
1574
|
+
const raw = firstStringValue(args.skill, args.skill_name, args.skillName, args.name, args.command).replace(/^\/+/, "").trim();
|
|
1575
|
+
const named = normalizedSkillUsage(raw);
|
|
1576
|
+
if (named) return named;
|
|
1577
|
+
}
|
|
1578
|
+
const candidates = [call?.argument, call?.rawInputSummary, call?.inputPreview, call?.target];
|
|
1579
|
+
const args = call?.arguments;
|
|
1580
|
+
if (args && typeof args === "object" && !Array.isArray(args)) {
|
|
1581
|
+
for (const value of Object.values(args)) {
|
|
1582
|
+
if (typeof value === "string") candidates.push(value);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
for (const text of candidates) {
|
|
1586
|
+
if (typeof text !== "string" || !text.includes("SKILL.md")) continue;
|
|
1587
|
+
const match = SKILL_FILE_PATH_RE.exec(text);
|
|
1588
|
+
if (match) return normalizedSkillUsage(match[1]);
|
|
1589
|
+
}
|
|
1590
|
+
return null;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
function normalizedSkillUsage(raw) {
|
|
1594
|
+
const label = String(raw || "").trim();
|
|
1595
|
+
if (!label) return null;
|
|
1596
|
+
const name = label.toLowerCase().replace(/[^a-z0-9._:-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
1597
|
+
if (!name) return null;
|
|
1598
|
+
return { skill_name: name, skill_label: label };
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
const MCP_GENERIC_TOOL_NAMES = new Set([
|
|
1602
|
+
"mcp",
|
|
1603
|
+
"mcp_tool",
|
|
1604
|
+
"mcp_tool_call",
|
|
1605
|
+
"call_mcp_tool",
|
|
1606
|
+
"list_mcp_resources",
|
|
1607
|
+
"list_mcp_resource_templates",
|
|
1608
|
+
"read_mcp_resource"
|
|
1609
|
+
]);
|
|
1610
|
+
|
|
1611
|
+
// Connector prefixes used by hosted MCP tool names like "github_fetch_file"
|
|
1612
|
+
// (claude.ai connectors, Codex apps) where the server name is not encoded.
|
|
1613
|
+
const MCP_CONNECTOR_PREFIX_RE = /^(github|gmail|gcal|gdrive|google_calendar|google_drive|slack|linear|notion|asana|jira|figma|stripe|vercel|supabase|sentry|atlassian)_./;
|
|
1614
|
+
|
|
1615
|
+
function mcpServerFromToolUsage(toolName, category) {
|
|
1616
|
+
if (/^mcp__/.test(toolName)) {
|
|
1617
|
+
const server = toolName.split("__").filter(Boolean)[1] || "";
|
|
1618
|
+
return server ? { server_name: server, server_label: server } : null;
|
|
1619
|
+
}
|
|
1620
|
+
if (category !== "mcp") return null;
|
|
1621
|
+
if (!toolName || MCP_GENERIC_TOOL_NAMES.has(toolName) || /^mcp[_-]*$/.test(toolName)) return null;
|
|
1622
|
+
const prefix = MCP_CONNECTOR_PREFIX_RE.exec(toolName);
|
|
1623
|
+
if (prefix) return { server_name: prefix[1], server_label: prefix[1] };
|
|
1624
|
+
return { server_name: toolName, server_label: toolName };
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
function normalizeToolUsageCategory(category, name) {
|
|
1628
|
+
const raw = String(category || "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
1629
|
+
if (raw) return raw;
|
|
1630
|
+
const key = normalizeToolUsageName(name);
|
|
1631
|
+
if (key.includes("search") || key.includes("grep") || key.includes("glob")) return "search";
|
|
1632
|
+
if (key.includes("web") || key.includes("browser") || key.includes("url")) return "web";
|
|
1633
|
+
if (key.includes("read") || key.includes("list")) return "read";
|
|
1634
|
+
if (key.includes("edit") || key.includes("write") || key.includes("patch")) return "edit";
|
|
1635
|
+
if (key.includes("bash") || key.includes("shell") || key.includes("command") || key.includes("exec")) return "shell";
|
|
1636
|
+
if (key.includes("task") || key.includes("todo") || key.includes("agent")) return "task";
|
|
1637
|
+
if (key.includes("mcp")) return "mcp";
|
|
1638
|
+
return "tool";
|
|
1639
|
+
}
|
|
1640
|
+
|
|
748
1641
|
function estimateSessionUsage(messages, options = {}) {
|
|
749
1642
|
const provider = String(options.provider || "").toLowerCase();
|
|
750
1643
|
if (provider === "cursor") return estimateCursorSessionUsage(messages, options);
|
|
@@ -932,14 +1825,17 @@ function usageTokenSummary(usage) {
|
|
|
932
1825
|
usage.total_token_count,
|
|
933
1826
|
usage.total
|
|
934
1827
|
));
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
firstUsageNumber(usage.cacheCreationInputTokens, usage.cache_creation_input_tokens),
|
|
1828
|
+
const cacheCreationInputTokens = positiveTokenNumber(firstUsageNumber(usage.cacheCreationInputTokens, usage.cache_creation_input_tokens));
|
|
1829
|
+
const cacheReadInputTokens = sumPositiveTokenNumbers(
|
|
938
1830
|
firstUsageNumber(usage.cacheReadInputTokens, usage.cache_read_input_tokens),
|
|
939
|
-
firstUsageNumber(usage.cacheReadTokens, usage.cache_read_tokens)
|
|
1831
|
+
firstUsageNumber(usage.cacheReadTokens, usage.cache_read_tokens)
|
|
1832
|
+
);
|
|
1833
|
+
const explicitCacheInputTokens = positiveTokenNumber(firstUsageNumber(usage.cacheInputTokens, usage.cache_input_tokens));
|
|
1834
|
+
const otherCacheInputTokens = sumPositiveTokenNumbers(
|
|
940
1835
|
firstUsageNumber(usage.cachedContentTokenCount, usage.cached_content_token_count),
|
|
941
1836
|
firstUsageNumber(usage.cachedTokens, usage.cached_tokens, usage.cacheTokens, usage.cache_tokens, usage.cached)
|
|
942
1837
|
);
|
|
1838
|
+
const cacheInputTokens = explicitCacheInputTokens || (cacheCreationInputTokens + cacheReadInputTokens + otherCacheInputTokens);
|
|
943
1839
|
const cacheInputTokensIncludedInInput =
|
|
944
1840
|
usage.cacheInputTokensIncludedInInput === true ||
|
|
945
1841
|
usage.cache_input_tokens_included_in_input === true ||
|
|
@@ -975,6 +1871,8 @@ function usageTokenSummary(usage) {
|
|
|
975
1871
|
totalInputTokens,
|
|
976
1872
|
totalOutputTokens,
|
|
977
1873
|
cacheInputTokens,
|
|
1874
|
+
cacheCreationInputTokens,
|
|
1875
|
+
cacheReadInputTokens,
|
|
978
1876
|
countedCacheInputTokens,
|
|
979
1877
|
cacheInputTokensIncludedInInput,
|
|
980
1878
|
reasoningOutputTokens,
|
|
@@ -1001,6 +1899,8 @@ function emptyUsageTokenSummary() {
|
|
|
1001
1899
|
totalInputTokens: 0,
|
|
1002
1900
|
totalOutputTokens: 0,
|
|
1003
1901
|
cacheInputTokens: 0,
|
|
1902
|
+
cacheCreationInputTokens: 0,
|
|
1903
|
+
cacheReadInputTokens: 0,
|
|
1004
1904
|
countedCacheInputTokens: 0,
|
|
1005
1905
|
cacheInputTokensIncludedInInput: false,
|
|
1006
1906
|
reasoningOutputTokens: 0,
|
|
@@ -1034,6 +1934,11 @@ function positiveTokenNumber(value) {
|
|
|
1034
1934
|
return Number.isFinite(number) && number > 0 ? number : 0;
|
|
1035
1935
|
}
|
|
1036
1936
|
|
|
1937
|
+
function positiveCostNumber(value) {
|
|
1938
|
+
const number = Number(value);
|
|
1939
|
+
return Number.isFinite(number) && number > 0 ? number : 0;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1037
1942
|
function sumPositiveTokenNumbers(...values) {
|
|
1038
1943
|
return values.reduce((sum, value) => sum + positiveTokenNumber(value), 0);
|
|
1039
1944
|
}
|
|
@@ -1095,6 +2000,12 @@ function mergeSessionUsage(primary, secondary) {
|
|
|
1095
2000
|
if (primary.cacheInputTokensIncludedInInput || secondary.cacheInputTokensIncludedInInput) {
|
|
1096
2001
|
result.cacheInputTokensIncludedInInput = true;
|
|
1097
2002
|
}
|
|
2003
|
+
if (primary.cacheCreationInputTokens || secondary.cacheCreationInputTokens) {
|
|
2004
|
+
result.cacheCreationInputTokens = primary.cacheCreationInputTokens || secondary.cacheCreationInputTokens || 0;
|
|
2005
|
+
}
|
|
2006
|
+
if (primary.cacheReadInputTokens || secondary.cacheReadInputTokens) {
|
|
2007
|
+
result.cacheReadInputTokens = primary.cacheReadInputTokens || secondary.cacheReadInputTokens || 0;
|
|
2008
|
+
}
|
|
1098
2009
|
if (primary.reasoningOutputTokensIncludedInOutput || secondary.reasoningOutputTokensIncludedInOutput) {
|
|
1099
2010
|
result.reasoningOutputTokensIncludedInOutput = true;
|
|
1100
2011
|
}
|
|
@@ -1104,6 +2015,13 @@ function mergeSessionUsage(primary, secondary) {
|
|
|
1104
2015
|
result.estimationMethod = primary.estimationMethod;
|
|
1105
2016
|
}
|
|
1106
2017
|
}
|
|
2018
|
+
const costUsd = positiveCostNumber(primary.costUsd) || positiveCostNumber(secondary.costUsd);
|
|
2019
|
+
if (costUsd) {
|
|
2020
|
+
result.costUsd = costUsd;
|
|
2021
|
+
result.currency = primary.currency || secondary.currency || "USD";
|
|
2022
|
+
result.costSource = primary.costSource || secondary.costSource || "provider-usage";
|
|
2023
|
+
result.costEstimated = Boolean(primary.costEstimated && secondary.costEstimated);
|
|
2024
|
+
}
|
|
1107
2025
|
return result;
|
|
1108
2026
|
}
|
|
1109
2027
|
|
|
@@ -1180,17 +2098,24 @@ function ensureConversationMarkdown(session, env = process.env) {
|
|
|
1180
2098
|
}
|
|
1181
2099
|
|
|
1182
2100
|
function conversationMarkdownNeedsRefresh(file, session) {
|
|
1183
|
-
if (session?.provider !== "chatgpt" && session?.sourceType !== "chatgpt-export") return false;
|
|
1184
2101
|
let text = "";
|
|
1185
2102
|
try {
|
|
1186
2103
|
text = fs.readFileSync(file, "utf8");
|
|
1187
2104
|
} catch {
|
|
1188
2105
|
return false;
|
|
1189
2106
|
}
|
|
2107
|
+
if (!conversationRenderVersionMatches(text)) return true;
|
|
2108
|
+
if (session?.provider !== "chatgpt" && session?.sourceType !== "chatgpt-export") return false;
|
|
1190
2109
|
return /\uE200[A-Za-z_]*cite\uE202[^\uE201]+\uE201/.test(text);
|
|
1191
2110
|
}
|
|
1192
2111
|
|
|
1193
|
-
const VIEW_SCHEMA_VERSION =
|
|
2112
|
+
const VIEW_SCHEMA_VERSION = 9;
|
|
2113
|
+
const CONVERSATION_RENDER_VERSION = 2;
|
|
2114
|
+
|
|
2115
|
+
function conversationRenderVersionMatches(text) {
|
|
2116
|
+
const match = /^conversation_render_version:\s*"?([^"\n]+)"?\s*$/m.exec(String(text || ""));
|
|
2117
|
+
return Boolean(match && Number(match[1]) === CONVERSATION_RENDER_VERSION);
|
|
2118
|
+
}
|
|
1194
2119
|
|
|
1195
2120
|
function sessionViewPathFromMetadata(metadataPath) {
|
|
1196
2121
|
if (!metadataPath) return "";
|
|
@@ -1234,6 +2159,51 @@ function displayTranscriptMessages(messages, time) {
|
|
|
1234
2159
|
return messages.map((message) => ({ ...message, timestamp: "" }));
|
|
1235
2160
|
}
|
|
1236
2161
|
|
|
2162
|
+
function compactViewTranscriptMessages(messages) {
|
|
2163
|
+
return (messages || []).map(compactViewTranscriptMessage);
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
function compactViewTranscriptMessage(message) {
|
|
2167
|
+
if (!message || typeof message !== "object") return message;
|
|
2168
|
+
const metadata = compactViewMetadata(message.metadata);
|
|
2169
|
+
if (metadata === message.metadata) return message;
|
|
2170
|
+
return { ...message, metadata };
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
function compactViewMetadata(metadata) {
|
|
2174
|
+
if (!metadata || typeof metadata !== "object") return metadata;
|
|
2175
|
+
let next = null;
|
|
2176
|
+
if (metadata.toolResult && typeof metadata.toolResult === "object") {
|
|
2177
|
+
next = { ...metadata, toolResult: compactViewToolResult(metadata.toolResult) };
|
|
2178
|
+
}
|
|
2179
|
+
return next || metadata;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
function compactViewToolResult(result) {
|
|
2183
|
+
if (!result || typeof result !== "object") return result;
|
|
2184
|
+
const next = { ...result };
|
|
2185
|
+
delete next.output;
|
|
2186
|
+
delete next.text;
|
|
2187
|
+
delete next.content;
|
|
2188
|
+
return next;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
function compactViewCanonicalEvents(events) {
|
|
2192
|
+
return (Array.isArray(events) ? events : []).map(compactViewCanonicalEvent);
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
function compactViewCanonicalEvent(event) {
|
|
2196
|
+
if (!event || typeof event !== "object") return event;
|
|
2197
|
+
const body = event.body && typeof event.body === "object" ? event.body : null;
|
|
2198
|
+
if (!body) return event;
|
|
2199
|
+
const nextBody = { ...body };
|
|
2200
|
+
if (typeof nextBody.text === "string") nextBody.text = "";
|
|
2201
|
+
if (nextBody.toolResult && typeof nextBody.toolResult === "object") {
|
|
2202
|
+
nextBody.toolResult = compactViewToolResult(nextBody.toolResult);
|
|
2203
|
+
}
|
|
2204
|
+
return { ...event, body: nextBody };
|
|
2205
|
+
}
|
|
2206
|
+
|
|
1237
2207
|
function cursorRawTimeMatchesSourceMtime(session, timestamp) {
|
|
1238
2208
|
const rawFiles = Array.isArray(session?.rawFiles) ? session.rawFiles : [];
|
|
1239
2209
|
return rawFiles.some((file) => {
|
|
@@ -1276,7 +2246,7 @@ function sessionHistoryTime(session) {
|
|
|
1276
2246
|
|
|
1277
2247
|
function computeSessionView(session, transcriptMessages, canonicalEvents) {
|
|
1278
2248
|
const time = sessionHistoryTime(session);
|
|
1279
|
-
const display = displayTranscriptMessages(dedupeTranscriptMessages(transcriptMessages || []), time);
|
|
2249
|
+
const display = compactViewTranscriptMessages(displayTranscriptMessages(dedupeTranscriptMessages(transcriptMessages || []), time));
|
|
1280
2250
|
return {
|
|
1281
2251
|
view_schema_version: VIEW_SCHEMA_VERSION,
|
|
1282
2252
|
session_id: session?.sessionId || "",
|
|
@@ -1285,7 +2255,7 @@ function computeSessionView(session, transcriptMessages, canonicalEvents) {
|
|
|
1285
2255
|
time_status: time.status || "",
|
|
1286
2256
|
session_summary: session?.sessionSummary || undefined,
|
|
1287
2257
|
transcript_messages: display,
|
|
1288
|
-
canonical_events:
|
|
2258
|
+
canonical_events: compactViewCanonicalEvents(canonicalEvents)
|
|
1289
2259
|
};
|
|
1290
2260
|
}
|
|
1291
2261
|
|
|
@@ -1325,6 +2295,44 @@ function renderConversationMarkdown(session, messages, events = []) {
|
|
|
1325
2295
|
const title = session.title || `${session.provider || "agent"} session ${session.sessionId}`;
|
|
1326
2296
|
const time = sessionHistoryTime(session);
|
|
1327
2297
|
const displayMessages = displayTranscriptMessages(messages, time);
|
|
2298
|
+
// Index events by messageIndex once so the per-message loop below is O(events)
|
|
2299
|
+
// total instead of O(messages * events) — for huge sessions the naive filter
|
|
2300
|
+
// per message is billions of comparisons.
|
|
2301
|
+
const toolCallsByMessageIndex = new Map();
|
|
2302
|
+
const toolResultsByMessageIndex = new Map();
|
|
2303
|
+
const memoryEventsByMessageIndex = new Map();
|
|
2304
|
+
const eventsById = new Map();
|
|
2305
|
+
for (const event of events) {
|
|
2306
|
+
if (!event || typeof event !== "object") continue;
|
|
2307
|
+
if (event.eventId) eventsById.set(event.eventId, event);
|
|
2308
|
+
}
|
|
2309
|
+
const memoryToolCallEventIds = new Set();
|
|
2310
|
+
const memoryToolResultEventIds = new Set();
|
|
2311
|
+
for (const event of events) {
|
|
2312
|
+
if (!event || typeof event !== "object") continue;
|
|
2313
|
+
if (isMemoryEventKind(event.kind)) {
|
|
2314
|
+
const list = memoryEventsByMessageIndex.get(event.messageIndex);
|
|
2315
|
+
if (list) list.push(event);
|
|
2316
|
+
else memoryEventsByMessageIndex.set(event.messageIndex, [event]);
|
|
2317
|
+
if (event.parentEventId) {
|
|
2318
|
+
memoryToolResultEventIds.add(event.parentEventId);
|
|
2319
|
+
const resultEvent = eventsById.get(event.parentEventId);
|
|
2320
|
+
if (resultEvent?.parentEventId) memoryToolCallEventIds.add(resultEvent.parentEventId);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
for (const event of events) {
|
|
2325
|
+
if (!event || typeof event !== "object") continue;
|
|
2326
|
+
const bucket = event.kind === "tool.called"
|
|
2327
|
+
? memoryToolCallEventIds.has(event.eventId) ? null : toolCallsByMessageIndex
|
|
2328
|
+
: event.kind === "tool.completed"
|
|
2329
|
+
? memoryToolResultEventIds.has(event.eventId) ? null : toolResultsByMessageIndex
|
|
2330
|
+
: null;
|
|
2331
|
+
if (!bucket) continue;
|
|
2332
|
+
const list = bucket.get(event.messageIndex);
|
|
2333
|
+
if (list) list.push(event);
|
|
2334
|
+
else bucket.set(event.messageIndex, [event]);
|
|
2335
|
+
}
|
|
1328
2336
|
const hasRedactions =
|
|
1329
2337
|
displayMessages.some((message) => /\[REDACTED(?::|\s+)[^\]\n]+\]/.test(JSON.stringify(message))) ||
|
|
1330
2338
|
events.some((event) => /\[REDACTED(?::|\s+)[^\]\n]+\]/.test(JSON.stringify(event)));
|
|
@@ -1332,6 +2340,9 @@ function renderConversationMarkdown(session, messages, events = []) {
|
|
|
1332
2340
|
"---",
|
|
1333
2341
|
`session_id: ${yamlString(session.sessionId)}`,
|
|
1334
2342
|
`provider: ${yamlString(session.provider)}`,
|
|
2343
|
+
`device_name: ${yamlString(session.device?.name || "")}`,
|
|
2344
|
+
`device_slug: ${yamlString(session.device?.slug || "")}`,
|
|
2345
|
+
`device_id: ${yamlString(session.device?.id || "")}`,
|
|
1335
2346
|
`repo: ${yamlString(session.repoCanonical || "")}`,
|
|
1336
2347
|
`scope: ${yamlString(session.scopeCanonical || "")}`,
|
|
1337
2348
|
`cwd: ${yamlString(session.cwd || "")}`,
|
|
@@ -1345,6 +2356,7 @@ function renderConversationMarkdown(session, messages, events = []) {
|
|
|
1345
2356
|
`conversation_kind: ${yamlString(session.conversationKind || "")}`,
|
|
1346
2357
|
`time_status: ${yamlString(time.status || session.timeStatus || "")}`,
|
|
1347
2358
|
`archive_schema_version: ${yamlString(session.archiveSchemaVersion || 1)}`,
|
|
2359
|
+
`conversation_render_version: ${yamlString(CONVERSATION_RENDER_VERSION)}`,
|
|
1348
2360
|
`event_count: ${yamlString(events.length || session.eventCount || 0)}`,
|
|
1349
2361
|
`parser_versions: ${yamlString(JSON.stringify(session.parserVersions || {}))}`,
|
|
1350
2362
|
"---",
|
|
@@ -1364,7 +2376,8 @@ function renderConversationMarkdown(session, messages, events = []) {
|
|
|
1364
2376
|
for (const message of displayMessages) {
|
|
1365
2377
|
body.push(`## ${roleLabel(message.role)} - ${message.timestamp || "unknown time"}`);
|
|
1366
2378
|
body.push("");
|
|
1367
|
-
const
|
|
2379
|
+
const suppressMemoryToolContent = String(message.role || "").toLowerCase() === "tool" && memoryEventsByMessageIndex.has(message.index);
|
|
2380
|
+
const content = suppressMemoryToolContent ? "" : styleRedactionMarkersForMarkdown(styleChatGptCitationMarkersForMarkdown(String(message.content || "").trim()));
|
|
1368
2381
|
if (content) {
|
|
1369
2382
|
body.push(content);
|
|
1370
2383
|
body.push("");
|
|
@@ -1374,22 +2387,116 @@ function renderConversationMarkdown(session, messages, events = []) {
|
|
|
1374
2387
|
body.push(attachmentMarkdown);
|
|
1375
2388
|
body.push("");
|
|
1376
2389
|
}
|
|
1377
|
-
for (const event of
|
|
2390
|
+
for (const event of toolCallsByMessageIndex.get(message.index) || []) {
|
|
1378
2391
|
body.push(`### Tool Call - ${event.occurredAt || message.timestamp || "unknown time"}`);
|
|
1379
2392
|
body.push("");
|
|
1380
2393
|
body.push(styleRedactionMarkersForMarkdown(styleChatGptCitationMarkersForMarkdown(String(event.body?.text || event.indexed?.summary || "").trim())));
|
|
1381
2394
|
body.push("");
|
|
1382
2395
|
}
|
|
1383
|
-
for (const event of
|
|
2396
|
+
for (const event of toolResultsByMessageIndex.get(message.index) || []) {
|
|
1384
2397
|
body.push(`### Tool Result - ${event.occurredAt || message.timestamp || "unknown time"}`);
|
|
1385
2398
|
body.push("");
|
|
1386
2399
|
body.push(styleRedactionMarkersForMarkdown(styleChatGptCitationMarkersForMarkdown(String(event.indexed?.summary || event.body?.text || "").trim())));
|
|
1387
2400
|
body.push("");
|
|
1388
2401
|
}
|
|
2402
|
+
for (const event of memoryEventsByMessageIndex.get(message.index) || []) {
|
|
2403
|
+
body.push(`### ${memoryMarkdownHeading(event)} - ${event.occurredAt || message.timestamp || "unknown time"}`);
|
|
2404
|
+
body.push("");
|
|
2405
|
+
body.push(styleRedactionMarkersForMarkdown(styleChatGptCitationMarkersForMarkdown(memoryMarkdownText(event, eventsById))));
|
|
2406
|
+
body.push("");
|
|
2407
|
+
}
|
|
1389
2408
|
}
|
|
1390
2409
|
return `${frontmatter.concat(body).join("\n").trimEnd()}\n`;
|
|
1391
2410
|
}
|
|
1392
2411
|
|
|
2412
|
+
function isMemoryEventKind(kind) {
|
|
2413
|
+
return kind === "memory.read" || kind === "memory.write" || kind === "memory.loaded";
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
function memoryMarkdownHeading(event) {
|
|
2417
|
+
const action = String(event?.indexed?.memoryAction || event?.body?.memory?.action || "").toLowerCase();
|
|
2418
|
+
if (action === "read") return "Memory Read";
|
|
2419
|
+
if (action === "write") return "Memory Write";
|
|
2420
|
+
return "Memory Loaded";
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
function memoryMarkdownText(event, eventsById = new Map()) {
|
|
2424
|
+
const memory = event?.body?.memory || {};
|
|
2425
|
+
const pathValue = String(event?.indexed?.memoryPath || memory.path || memory.name || "memory").trim();
|
|
2426
|
+
const details = [];
|
|
2427
|
+
if (pathValue) details.push(`Path: ${pathValue}`);
|
|
2428
|
+
if (event?.indexed?.memorySource || memory.source) details.push(`Source: ${event.indexed?.memorySource || memory.source}`);
|
|
2429
|
+
if (event?.indexed?.memoryScope || memory.scope) details.push(`Scope: ${event.indexed?.memoryScope || memory.scope}`);
|
|
2430
|
+
const diff = memoryMarkdownDiff(event, eventsById);
|
|
2431
|
+
const text = details.length ? details.join("\n") : String(event?.body?.text || event?.indexed?.summary || "").trim();
|
|
2432
|
+
return [text, diff].filter(Boolean).join("\n\n");
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
function memoryMarkdownDiff(event, eventsById = new Map()) {
|
|
2436
|
+
const completedEvent = event?.parentEventId ? eventsById.get(event.parentEventId) : null;
|
|
2437
|
+
const callEvent = completedEvent?.parentEventId ? eventsById.get(completedEvent.parentEventId) : null;
|
|
2438
|
+
const result = completedEvent?.body?.toolResult || {};
|
|
2439
|
+
const call = callEvent?.body?.toolCall || {};
|
|
2440
|
+
const hunks = firstStructuredPatch(result.structuredPatch, call.structuredPatch, call.arguments?.structuredPatch);
|
|
2441
|
+
if (hunks.length) return fencedMarkdown("diff", structuredPatchToDiffText(hunks));
|
|
2442
|
+
const editDiff = editArgumentsToDiffText(call.arguments);
|
|
2443
|
+
if (editDiff) return fencedMarkdown("diff", editDiff);
|
|
2444
|
+
return "";
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
function firstStructuredPatch(...values) {
|
|
2448
|
+
for (const value of values) {
|
|
2449
|
+
if (Array.isArray(value) && value.length) return value;
|
|
2450
|
+
}
|
|
2451
|
+
return [];
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
function structuredPatchToDiffText(hunks) {
|
|
2455
|
+
const lines = [];
|
|
2456
|
+
let previousFile = "";
|
|
2457
|
+
for (const hunk of hunks) {
|
|
2458
|
+
const file = String(hunk?.file || "").trim();
|
|
2459
|
+
if (file && file !== previousFile) {
|
|
2460
|
+
lines.push(`--- ${file}`, `+++ ${file}`);
|
|
2461
|
+
previousFile = file;
|
|
2462
|
+
}
|
|
2463
|
+
const oldStart = Number(hunk?.oldStart) || 1;
|
|
2464
|
+
const newStart = Number(hunk?.newStart) || 1;
|
|
2465
|
+
lines.push(`@@ -${oldStart} +${newStart} @@`);
|
|
2466
|
+
for (const raw of Array.isArray(hunk?.lines) ? hunk.lines : []) lines.push(String(raw ?? ""));
|
|
2467
|
+
}
|
|
2468
|
+
return lines.join("\n").trim();
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
function editArgumentsToDiffText(args) {
|
|
2472
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return "";
|
|
2473
|
+
const edits = Array.isArray(args.edits) && args.edits.length ? args.edits : [args];
|
|
2474
|
+
const blocks = [];
|
|
2475
|
+
for (const edit of edits) {
|
|
2476
|
+
const oldString = typeof edit?.old_string === "string" ? edit.old_string : "";
|
|
2477
|
+
const newString = typeof edit?.new_string === "string" ? edit.new_string : "";
|
|
2478
|
+
if (!oldString && !newString) continue;
|
|
2479
|
+
blocks.push(simpleDiffText(oldString, newString));
|
|
2480
|
+
}
|
|
2481
|
+
if (blocks.length) return blocks.join("\n");
|
|
2482
|
+
const content = typeof args.content === "string" ? args.content : "";
|
|
2483
|
+
if (!content) return "";
|
|
2484
|
+
return content.split("\n").map((line) => `+${line}`).join("\n");
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
function simpleDiffText(oldString, newString) {
|
|
2488
|
+
const lines = [];
|
|
2489
|
+
for (const line of String(oldString || "").split("\n")) lines.push(`-${line}`);
|
|
2490
|
+
for (const line of String(newString || "").split("\n")) lines.push(`+${line}`);
|
|
2491
|
+
return lines.join("\n").trim();
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
function fencedMarkdown(language, value) {
|
|
2495
|
+
const text = String(value || "").trim();
|
|
2496
|
+
if (!text) return "";
|
|
2497
|
+
return `\`\`\`${language}\n${text.replace(/```/g, "`\u200b``")}\n\`\`\``;
|
|
2498
|
+
}
|
|
2499
|
+
|
|
1393
2500
|
function styleChatGptCitationMarkersForMarkdown(value) {
|
|
1394
2501
|
return String(value || "").replace(/\uE200([A-Za-z_]*cite)\uE202([^\uE201]+)\uE201/g, (_, kind, ref) => {
|
|
1395
2502
|
const label = /file/i.test(String(kind || "")) ? "file citation" : "citation";
|
|
@@ -1494,7 +2601,8 @@ function touchManifest(session, pathsForSession, env = process.env) {
|
|
|
1494
2601
|
transcriptPath: pathsForSession.transcriptPath,
|
|
1495
2602
|
eventPath: pathsForSession.eventPath,
|
|
1496
2603
|
viewPath: pathsForSession.viewPath || "",
|
|
1497
|
-
rawPath: pathsForSession.rawPath || ""
|
|
2604
|
+
rawPath: pathsForSession.rawPath || "",
|
|
2605
|
+
artifactsPath: pathsForSession.artifactsPath || ""
|
|
1498
2606
|
});
|
|
1499
2607
|
writeJson(manifestPath, { sessions: next.sort((a, b) => String(b.startedAt).localeCompare(String(a.startedAt))) });
|
|
1500
2608
|
}
|
|
@@ -1535,6 +2643,8 @@ function walk(dir, visit) {
|
|
|
1535
2643
|
|
|
1536
2644
|
module.exports = {
|
|
1537
2645
|
archiveRoot,
|
|
2646
|
+
collectSessionToolUsage,
|
|
2647
|
+
mcpServerFromToolUsage,
|
|
1538
2648
|
collectSessionModels,
|
|
1539
2649
|
computeSessionUsage,
|
|
1540
2650
|
computeSessionView,
|
|
@@ -1559,6 +2669,7 @@ module.exports = {
|
|
|
1559
2669
|
readEvents,
|
|
1560
2670
|
readTranscript,
|
|
1561
2671
|
renderConversationMarkdown,
|
|
2672
|
+
rebuildSessionListIndex,
|
|
1562
2673
|
revealSession,
|
|
1563
2674
|
sessionHistoryTime,
|
|
1564
2675
|
sessionViewPathForSession,
|