@claude-sessions/core 0.4.6 → 0.4.7-beta.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/dist/index.js CHANGED
@@ -66,14 +66,13 @@ var extractTitle = (input) => {
66
66
  text = extractTextContent(input, { stripIdeTags: true });
67
67
  }
68
68
  if (!text) return "Untitled";
69
- const { name, args } = parseCommandMessage(text);
70
- if (name) return args ? `${name} ${args}` : name;
71
- let cleaned = text.trim();
72
- if (!cleaned) return "Untitled";
73
- if (cleaned.includes("\n\n")) {
74
- cleaned = cleaned.split("\n\n")[0];
69
+ let firstParagraph = text.trim();
70
+ if (firstParagraph.includes("\n\n")) {
71
+ firstParagraph = firstParagraph.split("\n\n")[0];
75
72
  }
76
- return cleaned || "Untitled";
73
+ const { name, args } = parseCommandMessage(firstParagraph);
74
+ if (name) return args ? `${name} ${args}` : name;
75
+ return firstParagraph || "Untitled";
77
76
  };
78
77
  var isInvalidApiKeyMessage = (msg) => {
79
78
  const text = extractTextContent(msg.message);
@@ -102,11 +101,12 @@ var getDisplayTitle = (customTitle, currentSummary, title, maxLength = 60, fallb
102
101
  return currentSummary.length > maxLength ? currentSummary.slice(0, maxLength - 3) + "..." : currentSummary;
103
102
  }
104
103
  if (title && title !== "Untitled") {
105
- if (title.includes("<command-name>")) {
106
- const { name, args } = parseCommandMessage(title);
104
+ const firstParagraph = title.includes("\n\n") ? title.split("\n\n")[0] : title;
105
+ if (firstParagraph.includes("<command-name>")) {
106
+ const { name, args } = parseCommandMessage(firstParagraph);
107
107
  if (name) return args ? `${name} ${args}` : name;
108
108
  }
109
- return title;
109
+ return firstParagraph;
110
110
  }
111
111
  return fallback;
112
112
  };
@@ -152,27 +152,32 @@ var tryParseJsonLine = (line, lineNumber, filePath) => {
152
152
  return JSON.parse(line);
153
153
  } catch {
154
154
  if (filePath) {
155
- console.warn(`Skipping invalid JSON at line ${lineNumber} in ${filePath}`);
155
+ logger.warn(`Skipping invalid JSON at line ${lineNumber} in ${filePath}`);
156
156
  }
157
157
  return null;
158
158
  }
159
159
  };
160
- var parseJsonlLines = (lines, filePath) => {
161
- return lines.map((line, idx) => {
160
+ var parseJsonlLines = (lines, filePath, { strict = false } = {}) => {
161
+ const results = [];
162
+ for (let idx = 0; idx < lines.length; idx++) {
162
163
  try {
163
- return JSON.parse(line);
164
+ results.push(JSON.parse(lines[idx]));
164
165
  } catch (e) {
165
166
  const err = e;
166
- throw new Error(`Failed to parse line ${idx + 1} in ${filePath}: ${err.message}`, {
167
- cause: e
168
- });
167
+ if (strict) {
168
+ throw new Error(`Failed to parse line ${idx + 1} in ${filePath}: ${err.message}`, {
169
+ cause: e
170
+ });
171
+ }
172
+ logger.warn(`Skipping malformed line ${idx + 1} in ${filePath}: ${err.message}`);
169
173
  }
170
- });
174
+ }
175
+ return results;
171
176
  };
172
- var readJsonlFile = (filePath) => Effect.gen(function* () {
177
+ var readJsonlFile = (filePath, options) => Effect.gen(function* () {
173
178
  const content = yield* Effect.tryPromise(() => fs.readFile(filePath, "utf-8"));
174
179
  const lines = content.trim().split("\n").filter(Boolean);
175
- return parseJsonlLines(lines, filePath);
180
+ return parseJsonlLines(lines, filePath, options);
176
181
  });
177
182
  var formatRelativeTime = (timestamp) => {
178
183
  const date = typeof timestamp === "string" ? new Date(timestamp) : new Date(timestamp);
@@ -333,14 +338,14 @@ var resolvePathFromClaudeConfig = async (folderName, fileSystem = fsp) => {
333
338
  };
334
339
  var folderNameToPath = async (folderName) => {
335
340
  const homeDir = os.homedir();
336
- const realPath = getRealPathFromSession(folderName);
337
- if (realPath) {
338
- return toRelativePath(realPath, homeDir);
339
- }
340
341
  const configPath = await resolvePathFromClaudeConfig(folderName);
341
342
  if (configPath) {
342
343
  return toRelativePath(configPath, homeDir);
343
344
  }
345
+ const realPath = getRealPathFromSession(folderName);
346
+ if (realPath) {
347
+ return toRelativePath(realPath, homeDir);
348
+ }
344
349
  const absolutePath = folderNameToDisplayPath(folderName);
345
350
  return toRelativePath(absolutePath, homeDir);
346
351
  };
@@ -1081,7 +1086,7 @@ function deleteMessageWithChainRepair(messages, targetId, targetType) {
1081
1086
  // src/session/crud.ts
1082
1087
  var updateSessionSummary = (projectName, sessionId, newSummary) => Effect5.gen(function* () {
1083
1088
  const filePath = path5.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
1084
- const messages = yield* readJsonlFile(filePath);
1089
+ const messages = yield* readJsonlFile(filePath, { strict: true });
1085
1090
  const summaryIdx = messages.findIndex((m) => m.type === "summary");
1086
1091
  if (summaryIdx >= 0) {
1087
1092
  messages[summaryIdx] = { ...messages[summaryIdx], summary: newSummary };
@@ -1160,7 +1165,7 @@ var readSession = (projectName, sessionId) => Effect5.gen(function* () {
1160
1165
  });
1161
1166
  var deleteMessage = (projectName, sessionId, messageUuid, targetType) => Effect5.gen(function* () {
1162
1167
  const filePath = path5.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
1163
- const messages = yield* readJsonlFile(filePath);
1168
+ const messages = yield* readJsonlFile(filePath, { strict: true });
1164
1169
  const result = deleteMessageWithChainRepair(messages, messageUuid, targetType);
1165
1170
  if (!result.deleted) {
1166
1171
  return { success: false, error: "Message not found" };
@@ -1171,7 +1176,7 @@ var deleteMessage = (projectName, sessionId, messageUuid, targetType) => Effect5
1171
1176
  });
1172
1177
  var restoreMessage = (projectName, sessionId, message, index) => Effect5.gen(function* () {
1173
1178
  const filePath = path5.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
1174
- const messages = yield* readJsonlFile(filePath);
1179
+ const messages = yield* readJsonlFile(filePath, { strict: true });
1175
1180
  const msgUuid = message.uuid ?? message.messageId;
1176
1181
  if (!msgUuid) {
1177
1182
  return { success: false, error: "Message has no uuid or messageId" };
@@ -1236,7 +1241,7 @@ var renameSession = (projectName, sessionId, newTitle) => Effect5.gen(function*
1236
1241
  if (lines.length === 0) {
1237
1242
  return { success: false, error: "Empty session" };
1238
1243
  }
1239
- const messages = parseJsonlLines(lines, filePath);
1244
+ const messages = parseJsonlLines(lines, filePath, { strict: true });
1240
1245
  const customTitleIdx = messages.findIndex((m) => m.type === "custom-title");
1241
1246
  if (customTitleIdx >= 0) {
1242
1247
  messages.splice(customTitleIdx, 1);
@@ -1286,7 +1291,7 @@ var moveSession = (sourceProject, sessionId, targetProject) => Effect5.gen(funct
1286
1291
  var splitSession = (projectName, sessionId, splitAtMessageUuid) => Effect5.gen(function* () {
1287
1292
  const projectPath = path5.join(getSessionsDir(), projectName);
1288
1293
  const filePath = path5.join(projectPath, `${sessionId}.jsonl`);
1289
- const allMessages = yield* readJsonlFile(filePath);
1294
+ const allMessages = yield* readJsonlFile(filePath, { strict: true });
1290
1295
  const splitIndex = allMessages.findIndex((m) => m.uuid === splitAtMessageUuid);
1291
1296
  if (splitIndex === -1) {
1292
1297
  return { success: false, error: "Message not found" };
@@ -1333,7 +1338,9 @@ var splitSession = (projectName, sessionId, splitAtMessageUuid) => Effect5.gen(f
1333
1338
  const agentContent = yield* Effect5.tryPromise(() => fs6.readFile(agentPath, "utf-8"));
1334
1339
  const agentLines = agentContent.trim().split("\n").filter(Boolean);
1335
1340
  if (agentLines.length === 0) continue;
1336
- const agentMessages = parseJsonlLines(agentLines, agentPath);
1341
+ const agentMessages = parseJsonlLines(agentLines, agentPath, {
1342
+ strict: true
1343
+ });
1337
1344
  const firstAgentMsg = agentMessages[0];
1338
1345
  if (firstAgentMsg.sessionId === sessionId) {
1339
1346
  const agentId = agentFile.replace("agent-", "").replace(".jsonl", "");
@@ -1359,7 +1366,7 @@ var splitSession = (projectName, sessionId, splitAtMessageUuid) => Effect5.gen(f
1359
1366
  });
1360
1367
  var repairChain = (projectName, sessionId) => Effect5.gen(function* () {
1361
1368
  const filePath = path5.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
1362
- const messages = yield* readJsonlFile(filePath);
1369
+ const messages = yield* readJsonlFile(filePath, { strict: true });
1363
1370
  const beforeResult = validateChain(messages);
1364
1371
  const repairCount = autoRepairChain(messages);
1365
1372
  if (repairCount > 0) {
@@ -1376,8 +1383,75 @@ var repairChain = (projectName, sessionId) => Effect5.gen(function* () {
1376
1383
 
1377
1384
  // src/session/tree.ts
1378
1385
  import { Effect as Effect6 } from "effect";
1386
+ import * as fs8 from "fs/promises";
1387
+ import * as path7 from "path";
1388
+
1389
+ // src/session/cache.ts
1379
1390
  import * as fs7 from "fs/promises";
1380
1391
  import * as path6 from "path";
1392
+ var log2 = createLogger("cache");
1393
+ var CACHE_VERSION = 1;
1394
+ var CACHE_FILENAME = ".tree-cache.json";
1395
+ var getCachePath = (projectName) => path6.join(getSessionsDir(), projectName, CACHE_FILENAME);
1396
+ var loadTreeCache = async (projectName) => {
1397
+ const cachePath = getCachePath(projectName);
1398
+ try {
1399
+ const raw = await fs7.readFile(cachePath, "utf-8");
1400
+ const parsed = JSON.parse(raw);
1401
+ if (parsed.version !== CACHE_VERSION) {
1402
+ log2.debug(`cache version mismatch (${parsed.version} !== ${CACHE_VERSION}), ignoring`);
1403
+ return null;
1404
+ }
1405
+ return parsed;
1406
+ } catch {
1407
+ return null;
1408
+ }
1409
+ };
1410
+ var writeTreeCache = async (projectName, cache) => {
1411
+ const cachePath = getCachePath(projectName);
1412
+ const tmpPath = cachePath + ".tmp";
1413
+ try {
1414
+ await fs7.writeFile(tmpPath, JSON.stringify(cache), "utf-8");
1415
+ await fs7.rename(tmpPath, cachePath);
1416
+ } catch (e) {
1417
+ log2.debug(`failed to write cache: ${e}`);
1418
+ try {
1419
+ await fs7.unlink(tmpPath);
1420
+ } catch {
1421
+ }
1422
+ }
1423
+ };
1424
+ var validateCache = (cache, sessionFileIds, currentMtimes) => {
1425
+ const cachedIds = new Set(Object.keys(cache.sessions));
1426
+ const currentIds = new Set(sessionFileIds);
1427
+ const changedSessionIds = [];
1428
+ const unchangedSessionIds = [];
1429
+ const newSessionIds = [];
1430
+ const deletedSessionIds = [];
1431
+ for (const id of currentIds) {
1432
+ if (!cachedIds.has(id)) {
1433
+ newSessionIds.push(id);
1434
+ } else {
1435
+ const cachedMtime = cache.sessions[id].fileMtime;
1436
+ const currentMtime = currentMtimes.get(id) ?? 0;
1437
+ if (Math.abs(cachedMtime - currentMtime) <= 1) {
1438
+ unchangedSessionIds.push(id);
1439
+ } else {
1440
+ changedSessionIds.push(id);
1441
+ }
1442
+ }
1443
+ }
1444
+ for (const id of cachedIds) {
1445
+ if (!currentIds.has(id)) {
1446
+ deletedSessionIds.push(id);
1447
+ }
1448
+ }
1449
+ const isFullHit = changedSessionIds.length === 0 && newSessionIds.length === 0 && deletedSessionIds.length === 0;
1450
+ return { isFullHit, changedSessionIds, unchangedSessionIds, deletedSessionIds, newSessionIds };
1451
+ };
1452
+
1453
+ // src/session/tree.ts
1454
+ var log3 = createLogger("tree");
1381
1455
  var sortSessions = (sessions, sort) => {
1382
1456
  return sessions.sort((a, b) => {
1383
1457
  let comparison = 0;
@@ -1417,9 +1491,9 @@ var sortSessions = (sessions, sort) => {
1417
1491
  });
1418
1492
  };
1419
1493
  var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSession, fileMtime) => Effect6.gen(function* () {
1420
- const projectPath = path6.join(getSessionsDir(), projectName);
1421
- const filePath = path6.join(projectPath, `${sessionId}.jsonl`);
1422
- const content = yield* Effect6.tryPromise(() => fs7.readFile(filePath, "utf-8"));
1494
+ const projectPath = path7.join(getSessionsDir(), projectName);
1495
+ const filePath = path7.join(projectPath, `${sessionId}.jsonl`);
1496
+ const content = yield* Effect6.tryPromise(() => fs8.readFile(filePath, "utf-8"));
1423
1497
  const lines = content.trim().split("\n").filter(Boolean);
1424
1498
  const messages = parseJsonlLines(lines, filePath);
1425
1499
  let summaries;
@@ -1437,12 +1511,12 @@ var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSess
1437
1511
  sessionUuids.add(msg.uuid);
1438
1512
  }
1439
1513
  }
1440
- const projectFiles = yield* Effect6.tryPromise(() => fs7.readdir(projectPath));
1514
+ const projectFiles = yield* Effect6.tryPromise(() => fs8.readdir(projectPath));
1441
1515
  const allJsonlFiles = projectFiles.filter((f) => f.endsWith(".jsonl"));
1442
1516
  for (const file of allJsonlFiles) {
1443
1517
  try {
1444
- const otherFilePath = path6.join(projectPath, file);
1445
- const otherContent = yield* Effect6.tryPromise(() => fs7.readFile(otherFilePath, "utf-8"));
1518
+ const otherFilePath = path7.join(projectPath, file);
1519
+ const otherContent = yield* Effect6.tryPromise(() => fs8.readFile(otherFilePath, "utf-8"));
1446
1520
  const otherLines = otherContent.trim().split("\n").filter(Boolean);
1447
1521
  for (let i = 0; i < otherLines.length; i++) {
1448
1522
  const msg = tryParseJsonLine(otherLines[i], i + 1, otherFilePath);
@@ -1486,9 +1560,9 @@ var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSess
1486
1560
  const linkedAgentIds = yield* findLinkedAgents(projectName, sessionId);
1487
1561
  const agents = [];
1488
1562
  for (const agentId of linkedAgentIds) {
1489
- const agentPath = path6.join(projectPath, `${agentId}.jsonl`);
1563
+ const agentPath = path7.join(projectPath, `${agentId}.jsonl`);
1490
1564
  try {
1491
- const agentContent = yield* Effect6.tryPromise(() => fs7.readFile(agentPath, "utf-8"));
1565
+ const agentContent = yield* Effect6.tryPromise(() => fs8.readFile(agentPath, "utf-8"));
1492
1566
  const agentLines = agentContent.trim().split("\n").filter(Boolean);
1493
1567
  const agentMsgs = agentLines.map((l) => JSON.parse(l));
1494
1568
  const agentUserAssistant = agentMsgs.filter(
@@ -1535,40 +1609,17 @@ var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSess
1535
1609
  };
1536
1610
  });
1537
1611
  var loadSessionTreeData = (projectName, sessionId) => loadSessionTreeDataInternal(projectName, sessionId, void 0);
1538
- var DEFAULT_SORT = { field: "summary", order: "desc" };
1539
- var loadProjectTreeData = (projectName, sortOptions) => Effect6.gen(function* () {
1540
- const project = (yield* listProjects).find((p) => p.name === projectName);
1541
- if (!project) {
1542
- return null;
1543
- }
1544
- const sort = sortOptions ?? DEFAULT_SORT;
1545
- const projectPath = path6.join(getSessionsDir(), projectName);
1546
- const files = yield* Effect6.tryPromise(() => fs7.readdir(projectPath));
1547
- const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
1548
- const fileMtimes = /* @__PURE__ */ new Map();
1549
- yield* Effect6.all(
1550
- sessionFiles.map(
1551
- (file) => Effect6.gen(function* () {
1552
- const filePath = path6.join(projectPath, file);
1553
- try {
1554
- const stat4 = yield* Effect6.tryPromise(() => fs7.stat(filePath));
1555
- fileMtimes.set(file.replace(".jsonl", ""), stat4.mtimeMs);
1556
- } catch {
1557
- }
1558
- })
1559
- ),
1560
- { concurrency: 20 }
1561
- );
1612
+ var DEFAULT_SORT = { field: "updated", order: "desc" };
1613
+ var buildPhase1 = (projectPath, allJsonlFiles) => Effect6.gen(function* () {
1562
1614
  const globalUuidMap = /* @__PURE__ */ new Map();
1563
1615
  const allSummaries = [];
1564
- const allJsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
1565
1616
  yield* Effect6.all(
1566
1617
  allJsonlFiles.map(
1567
1618
  (file) => Effect6.gen(function* () {
1568
- const filePath = path6.join(projectPath, file);
1619
+ const filePath = path7.join(projectPath, file);
1569
1620
  const fileSessionId = file.replace(".jsonl", "");
1570
1621
  try {
1571
- const content = yield* Effect6.tryPromise(() => fs7.readFile(filePath, "utf-8"));
1622
+ const content = yield* Effect6.tryPromise(() => fs8.readFile(filePath, "utf-8"));
1572
1623
  const lines = content.trim().split("\n").filter(Boolean);
1573
1624
  for (let i = 0; i < lines.length; i++) {
1574
1625
  const msg = tryParseJsonLine(lines[i], i + 1, filePath);
@@ -1600,6 +1651,9 @@ var loadProjectTreeData = (projectName, sortOptions) => Effect6.gen(function* ()
1600
1651
  ),
1601
1652
  { concurrency: 20 }
1602
1653
  );
1654
+ return { globalUuidMap, allSummaries };
1655
+ });
1656
+ var buildSummariesByTargetSession = (globalUuidMap, allSummaries) => {
1603
1657
  const summariesByTargetSession = /* @__PURE__ */ new Map();
1604
1658
  for (const summaryData of allSummaries) {
1605
1659
  if (summaryData.leafUuid) {
@@ -1612,21 +1666,22 @@ var loadProjectTreeData = (projectName, sortOptions) => Effect6.gen(function* ()
1612
1666
  summariesByTargetSession.get(targetSessionId).push({
1613
1667
  summary: summaryData.summary,
1614
1668
  leafUuid: summaryData.leafUuid,
1615
- // Use summary's own timestamp for sorting, not the target message's timestamp
1616
1669
  timestamp: summaryData.timestamp ?? targetInfo.timestamp,
1617
1670
  sourceFile: summaryData.sourceFile
1618
1671
  });
1619
1672
  }
1620
1673
  }
1621
1674
  }
1622
- const sessions = yield* Effect6.all(
1623
- sessionFiles.map((file) => {
1624
- const sessionId = file.replace(".jsonl", "");
1625
- const mtime = fileMtimes.get(sessionId);
1626
- return loadSessionTreeDataInternal(projectName, sessionId, summariesByTargetSession, mtime);
1627
- }),
1628
- { concurrency: 10 }
1629
- );
1675
+ return summariesByTargetSession;
1676
+ };
1677
+ var sortSummaries = (summaries) => {
1678
+ return [...summaries].sort((a, b) => {
1679
+ const timestampCmp = (a.timestamp ?? "").localeCompare(b.timestamp ?? "");
1680
+ if (timestampCmp !== 0) return timestampCmp;
1681
+ return (b.sourceFile ?? "").localeCompare(a.sourceFile ?? "");
1682
+ });
1683
+ };
1684
+ var buildProjectTreeResult = (project, sessions, sort) => {
1630
1685
  const sortedSessions = sortSessions(sessions, sort);
1631
1686
  const filteredSessions = sortedSessions.filter((s) => {
1632
1687
  if (isErrorSessionTitle(s.title)) return false;
@@ -1641,12 +1696,165 @@ var loadProjectTreeData = (projectName, sortOptions) => Effect6.gen(function* ()
1641
1696
  sessionCount: filteredSessions.length,
1642
1697
  sessions: filteredSessions
1643
1698
  };
1699
+ };
1700
+ var buildTreeCache = (globalUuidMap, allSummaries, sessions, fileMtimes) => {
1701
+ const uuidMapRecord = {};
1702
+ for (const [key, val] of globalUuidMap) {
1703
+ uuidMapRecord[key] = val;
1704
+ }
1705
+ const sessionsRecord = {};
1706
+ for (const s of sessions) {
1707
+ sessionsRecord[s.id] = {
1708
+ fileMtime: fileMtimes.get(s.id) ?? 0,
1709
+ data: s
1710
+ };
1711
+ }
1712
+ return {
1713
+ version: 1,
1714
+ globalUuidMap: uuidMapRecord,
1715
+ allSummaries,
1716
+ sessions: sessionsRecord
1717
+ };
1718
+ };
1719
+ var updateSessionSummaries = (cached, summariesByTargetSession) => {
1720
+ const newSummaries = sortSummaries(summariesByTargetSession.get(cached.id) ?? []);
1721
+ const oldJson = JSON.stringify(cached.summaries);
1722
+ const newJson = JSON.stringify(newSummaries);
1723
+ if (oldJson === newJson) return cached;
1724
+ const newSortTimestamp = getSessionSortTimestamp({
1725
+ summaries: newSummaries,
1726
+ createdAt: cached.createdAt
1727
+ });
1728
+ return {
1729
+ ...cached,
1730
+ summaries: newSummaries,
1731
+ currentSummary: newSummaries[0]?.summary,
1732
+ sortTimestamp: newSortTimestamp
1733
+ };
1734
+ };
1735
+ var loadProjectTreeData = (projectName, sortOptions) => Effect6.gen(function* () {
1736
+ const projectPath = path7.join(getSessionsDir(), projectName);
1737
+ const exists = yield* Effect6.tryPromise(
1738
+ () => fs8.access(projectPath).then(() => true).catch(() => false)
1739
+ );
1740
+ if (!exists) return null;
1741
+ const displayName = yield* Effect6.tryPromise(() => folderNameToPath(projectName));
1742
+ const project = { name: projectName, displayName, path: projectPath };
1743
+ const sort = sortOptions ?? DEFAULT_SORT;
1744
+ const files = yield* Effect6.tryPromise(() => fs8.readdir(projectPath));
1745
+ const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
1746
+ const sessionFileIds = sessionFiles.map((f) => f.replace(".jsonl", ""));
1747
+ const fileMtimes = /* @__PURE__ */ new Map();
1748
+ yield* Effect6.all(
1749
+ sessionFiles.map(
1750
+ (file) => Effect6.gen(function* () {
1751
+ const filePath = path7.join(projectPath, file);
1752
+ try {
1753
+ const stat4 = yield* Effect6.tryPromise(() => fs8.stat(filePath));
1754
+ fileMtimes.set(file.replace(".jsonl", ""), stat4.mtimeMs);
1755
+ } catch {
1756
+ }
1757
+ })
1758
+ ),
1759
+ { concurrency: 20 }
1760
+ );
1761
+ const cache = yield* Effect6.tryPromise(() => loadTreeCache(projectName));
1762
+ if (cache) {
1763
+ const validation = validateCache(cache, sessionFileIds, fileMtimes);
1764
+ if (validation.isFullHit) {
1765
+ log3.debug(`cache hit for ${projectName} (${sessionFileIds.length} sessions)`);
1766
+ const sessions = sessionFileIds.map((id) => cache.sessions[id]?.data).filter((s) => s != null);
1767
+ return buildProjectTreeResult(project, sessions, sort);
1768
+ }
1769
+ const changedCount = validation.changedSessionIds.length + validation.newSessionIds.length + validation.deletedSessionIds.length;
1770
+ log3.debug(
1771
+ `cache partial miss for ${projectName}: ${validation.changedSessionIds.length} changed, ${validation.newSessionIds.length} new, ${validation.deletedSessionIds.length} deleted, ${validation.unchangedSessionIds.length} unchanged`
1772
+ );
1773
+ if (changedCount <= sessionFileIds.length / 2) {
1774
+ const result = yield* loadProjectTreeDataIncremental(
1775
+ projectName,
1776
+ projectPath,
1777
+ files,
1778
+ sessionFiles,
1779
+ fileMtimes,
1780
+ cache,
1781
+ validation,
1782
+ project,
1783
+ sort
1784
+ );
1785
+ return result;
1786
+ }
1787
+ }
1788
+ log3.debug(`full load for ${projectName} (${sessionFileIds.length} sessions)`);
1789
+ return yield* loadProjectTreeDataFull(
1790
+ projectName,
1791
+ projectPath,
1792
+ files,
1793
+ sessionFiles,
1794
+ fileMtimes,
1795
+ project,
1796
+ sort
1797
+ );
1798
+ });
1799
+ var loadProjectTreeDataFull = (projectName, projectPath, files, sessionFiles, fileMtimes, project, sort) => Effect6.gen(function* () {
1800
+ const allJsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
1801
+ const { globalUuidMap, allSummaries } = yield* buildPhase1(projectPath, allJsonlFiles);
1802
+ const summariesByTargetSession = buildSummariesByTargetSession(globalUuidMap, allSummaries);
1803
+ const sessions = yield* Effect6.all(
1804
+ sessionFiles.map((file) => {
1805
+ const sessionId = file.replace(".jsonl", "");
1806
+ const mtime = fileMtimes.get(sessionId);
1807
+ return loadSessionTreeDataInternal(projectName, sessionId, summariesByTargetSession, mtime);
1808
+ }),
1809
+ { concurrency: 10 }
1810
+ );
1811
+ const cacheData = buildTreeCache(globalUuidMap, allSummaries, sessions, fileMtimes);
1812
+ writeTreeCache(projectName, cacheData).catch((err) => {
1813
+ log3.debug(`cache write failed for ${projectName}: ${err}`);
1814
+ });
1815
+ return buildProjectTreeResult(project, sessions, sort);
1816
+ });
1817
+ var loadProjectTreeDataIncremental = (projectName, projectPath, files, sessionFiles, fileMtimes, cache, validation, project, sort) => Effect6.gen(function* () {
1818
+ const allJsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
1819
+ const { globalUuidMap, allSummaries } = yield* buildPhase1(projectPath, allJsonlFiles);
1820
+ const summariesByTargetSession = buildSummariesByTargetSession(globalUuidMap, allSummaries);
1821
+ const sessionsToLoad = [...validation.changedSessionIds, ...validation.newSessionIds];
1822
+ const loadedSessions = yield* Effect6.all(
1823
+ sessionsToLoad.map((sessionId) => {
1824
+ const mtime = fileMtimes.get(sessionId);
1825
+ return loadSessionTreeDataInternal(projectName, sessionId, summariesByTargetSession, mtime);
1826
+ }),
1827
+ { concurrency: 10 }
1828
+ );
1829
+ const loadedMap = /* @__PURE__ */ new Map();
1830
+ for (const s of loadedSessions) {
1831
+ loadedMap.set(s.id, s);
1832
+ }
1833
+ const allSessions = [];
1834
+ for (const file of sessionFiles) {
1835
+ const sessionId = file.replace(".jsonl", "");
1836
+ const loaded = loadedMap.get(sessionId);
1837
+ if (loaded) {
1838
+ allSessions.push(loaded);
1839
+ } else if (cache.sessions[sessionId]) {
1840
+ const updated = updateSessionSummaries(
1841
+ cache.sessions[sessionId].data,
1842
+ summariesByTargetSession
1843
+ );
1844
+ allSessions.push({ ...updated, fileMtime: fileMtimes.get(sessionId) });
1845
+ }
1846
+ }
1847
+ const cacheData = buildTreeCache(globalUuidMap, allSummaries, allSessions, fileMtimes);
1848
+ writeTreeCache(projectName, cacheData).catch((err) => {
1849
+ log3.debug(`cache write failed for ${projectName}: ${err}`);
1850
+ });
1851
+ return buildProjectTreeResult(project, allSessions, sort);
1644
1852
  });
1645
1853
 
1646
1854
  // src/session/analysis.ts
1647
1855
  import { Effect as Effect7 } from "effect";
1648
- import * as fs8 from "fs/promises";
1649
- import * as path7 from "path";
1856
+ import * as fs9 from "fs/promises";
1857
+ import * as path8 from "path";
1650
1858
  var isToolUse = (item) => item !== null && typeof item === "object" && "type" in item && item.type === "tool_use";
1651
1859
  var isToolResultError = (item) => item !== null && typeof item === "object" && "type" in item && item.type === "tool_result" && "is_error" in item && item.is_error === true;
1652
1860
  var findToolUseById = (messages, toolUseId) => {
@@ -1777,11 +1985,11 @@ var analyzeSession = (projectName, sessionId) => Effect7.gen(function* () {
1777
1985
  });
1778
1986
  var compressSession = (projectName, sessionId, options = {}) => Effect7.gen(function* () {
1779
1987
  const { keepSnapshots = "first_last", maxToolOutputLength = 5e3 } = options;
1780
- const filePath = path7.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
1781
- const content = yield* Effect7.tryPromise(() => fs8.readFile(filePath, "utf-8"));
1988
+ const filePath = path8.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
1989
+ const content = yield* Effect7.tryPromise(() => fs9.readFile(filePath, "utf-8"));
1782
1990
  const originalSize = Buffer.byteLength(content, "utf-8");
1783
1991
  const lines = content.trim().split("\n").filter(Boolean);
1784
- const messages = parseJsonlLines(lines, filePath);
1992
+ const messages = parseJsonlLines(lines, filePath, { strict: true });
1785
1993
  let removedCustomTitles = 0;
1786
1994
  let removedProgress = 0;
1787
1995
  let removedSnapshots = 0;
@@ -1843,7 +2051,7 @@ var compressSession = (projectName, sessionId, options = {}) => Effect7.gen(func
1843
2051
  }
1844
2052
  const newContent = filteredMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
1845
2053
  const compressedSize = Buffer.byteLength(newContent, "utf-8");
1846
- yield* Effect7.tryPromise(() => fs8.writeFile(filePath, newContent, "utf-8"));
2054
+ yield* Effect7.tryPromise(() => fs9.writeFile(filePath, newContent, "utf-8"));
1847
2055
  return {
1848
2056
  success: true,
1849
2057
  originalSize,
@@ -1856,10 +2064,10 @@ var compressSession = (projectName, sessionId, options = {}) => Effect7.gen(func
1856
2064
  });
1857
2065
  var extractProjectKnowledge = (projectName, sessionIds) => Effect7.gen(function* () {
1858
2066
  const sessionsDir = getSessionsDir();
1859
- const projectDir = path7.join(sessionsDir, projectName);
2067
+ const projectDir = path8.join(sessionsDir, projectName);
1860
2068
  let targetSessionIds = sessionIds;
1861
2069
  if (!targetSessionIds) {
1862
- const files = yield* Effect7.tryPromise(() => fs8.readdir(projectDir));
2070
+ const files = yield* Effect7.tryPromise(() => fs9.readdir(projectDir));
1863
2071
  targetSessionIds = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-")).map((f) => f.replace(".jsonl", ""));
1864
2072
  }
1865
2073
  const fileModifyCount = /* @__PURE__ */ new Map();
@@ -1986,11 +2194,11 @@ var summarizeSession = (projectName, sessionId, options = {}) => Effect7.gen(fun
1986
2194
 
1987
2195
  // src/session/cleanup.ts
1988
2196
  import { Effect as Effect8 } from "effect";
1989
- import * as fs9 from "fs/promises";
1990
- import * as path8 from "path";
2197
+ import * as fs10 from "fs/promises";
2198
+ import * as path9 from "path";
1991
2199
  var cleanInvalidMessages = (projectName, sessionId) => Effect8.gen(function* () {
1992
- const filePath = path8.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
1993
- const content = yield* Effect8.tryPromise(() => fs9.readFile(filePath, "utf-8"));
2200
+ const filePath = path9.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
2201
+ const content = yield* Effect8.tryPromise(() => fs10.readFile(filePath, "utf-8"));
1994
2202
  const lines = content.trim().split("\n").filter(Boolean);
1995
2203
  if (lines.length === 0) return { removedCount: 0, remainingCount: 0 };
1996
2204
  const messages = parseJsonlLines(lines, filePath);
@@ -2022,7 +2230,7 @@ var cleanInvalidMessages = (projectName, sessionId) => Effect8.gen(function* ()
2022
2230
  lastValidUuid = msg.uuid;
2023
2231
  }
2024
2232
  const newContent = filtered.length > 0 ? filtered.map((m) => JSON.stringify(m)).join("\n") + "\n" : "";
2025
- yield* Effect8.tryPromise(() => fs9.writeFile(filePath, newContent, "utf-8"));
2233
+ yield* Effect8.tryPromise(() => fs10.writeFile(filePath, newContent, "utf-8"));
2026
2234
  const remainingUserAssistant = filtered.filter(
2027
2235
  (m) => m.type === "user" || m.type === "assistant"
2028
2236
  ).length;
@@ -2088,8 +2296,8 @@ var clearSessions = (options) => Effect8.gen(function* () {
2088
2296
  const sessionsToDelete = [];
2089
2297
  if (clearInvalid) {
2090
2298
  for (const project of targetProjects) {
2091
- const projectPath = path8.join(getSessionsDir(), project.name);
2092
- const files = yield* Effect8.tryPromise(() => fs9.readdir(projectPath));
2299
+ const projectPath = path9.join(getSessionsDir(), project.name);
2300
+ const files = yield* Effect8.tryPromise(() => fs10.readdir(projectPath));
2093
2301
  const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
2094
2302
  for (const file of sessionFiles) {
2095
2303
  const sessionId = file.replace(".jsonl", "");
@@ -2146,8 +2354,8 @@ var clearSessions = (options) => Effect8.gen(function* () {
2146
2354
 
2147
2355
  // src/session/search.ts
2148
2356
  import { Effect as Effect9, pipe as pipe2 } from "effect";
2149
- import * as fs10 from "fs/promises";
2150
- import * as path9 from "path";
2357
+ import * as fs11 from "fs/promises";
2358
+ import * as path10 from "path";
2151
2359
  var extractSnippet = (text, matchIndex, queryLength) => {
2152
2360
  const start = Math.max(0, matchIndex - 50);
2153
2361
  const end = Math.min(text.length, matchIndex + queryLength + 50);
@@ -2171,7 +2379,7 @@ var findContentMatch = (lines, queryLower, filePath) => {
2171
2379
  return null;
2172
2380
  };
2173
2381
  var searchSessionContent = (projectName, sessionId, filePath, queryLower) => pipe2(
2174
- Effect9.tryPromise(() => fs10.readFile(filePath, "utf-8")),
2382
+ Effect9.tryPromise(() => fs11.readFile(filePath, "utf-8")),
2175
2383
  Effect9.map((content) => {
2176
2384
  const lines = content.trim().split("\n").filter(Boolean);
2177
2385
  const match = findContentMatch(lines, queryLower, filePath);
@@ -2189,12 +2397,12 @@ var searchSessionContent = (projectName, sessionId, filePath, queryLower) => pip
2189
2397
  Effect9.catchAll(() => Effect9.succeed(null))
2190
2398
  );
2191
2399
  var searchProjectContent = (project, queryLower, alreadyFoundIds) => Effect9.gen(function* () {
2192
- const projectPath = path9.join(getSessionsDir(), project.name);
2193
- const files = yield* Effect9.tryPromise(() => fs10.readdir(projectPath));
2400
+ const projectPath = path10.join(getSessionsDir(), project.name);
2401
+ const files = yield* Effect9.tryPromise(() => fs11.readdir(projectPath));
2194
2402
  const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
2195
2403
  const searchEffects = sessionFiles.map((file) => ({
2196
2404
  sessionId: file.replace(".jsonl", ""),
2197
- filePath: path9.join(projectPath, file)
2405
+ filePath: path10.join(projectPath, file)
2198
2406
  })).filter(({ sessionId }) => !alreadyFoundIds.has(`${project.name}:${sessionId}`)).map(
2199
2407
  ({ sessionId, filePath }) => searchSessionContent(project.name, sessionId, filePath, queryLower)
2200
2408
  );
@@ -2298,12 +2506,12 @@ var getSessionFiles = (projectName, sessionId) => Effect10.gen(function* () {
2298
2506
 
2299
2507
  // src/session/index-file.ts
2300
2508
  import { Effect as Effect11 } from "effect";
2301
- import * as fs11 from "fs/promises";
2302
- import * as path10 from "path";
2509
+ import * as fs12 from "fs/promises";
2510
+ import * as path11 from "path";
2303
2511
  var loadSessionsIndex = (projectName) => Effect11.gen(function* () {
2304
- const indexPath = path10.join(getSessionsDir(), projectName, "sessions-index.json");
2512
+ const indexPath = path11.join(getSessionsDir(), projectName, "sessions-index.json");
2305
2513
  try {
2306
- const content = yield* Effect11.tryPromise(() => fs11.readFile(indexPath, "utf-8"));
2514
+ const content = yield* Effect11.tryPromise(() => fs12.readFile(indexPath, "utf-8"));
2307
2515
  const index = JSON.parse(content);
2308
2516
  return index;
2309
2517
  } catch {
@@ -2331,9 +2539,9 @@ var sortIndexEntriesByModified = (entries) => {
2331
2539
  });
2332
2540
  };
2333
2541
  var hasSessionsIndex = (projectName) => Effect11.gen(function* () {
2334
- const indexPath = path10.join(getSessionsDir(), projectName, "sessions-index.json");
2542
+ const indexPath = path11.join(getSessionsDir(), projectName, "sessions-index.json");
2335
2543
  try {
2336
- yield* Effect11.tryPromise(() => fs11.access(indexPath));
2544
+ yield* Effect11.tryPromise(() => fs12.access(indexPath));
2337
2545
  return true;
2338
2546
  } catch {
2339
2547
  return false;
@@ -2366,6 +2574,7 @@ export {
2366
2574
  folderNameToPath,
2367
2575
  formatRelativeTime,
2368
2576
  generateTreeNodeId,
2577
+ getCachePath,
2369
2578
  getDisplayTitle,
2370
2579
  getIndexEntryDisplayTitle,
2371
2580
  getLogger,