session-steward 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,8 @@ import {
8
8
  formatSessionForJson,
9
9
  getSessionOverview,
10
10
  getSessionRecord,
11
+ invalidateSessionCache,
12
+ listSessionDeletionBackups,
11
13
  listSessions,
12
14
  loadDeletionStore,
13
15
  loadSessionStore,
@@ -29,6 +31,8 @@ export const codexProvider = Object.freeze({
29
31
  formatSessionForJson,
30
32
  getSessionOverview,
31
33
  getSessionRecord,
34
+ invalidateSessionCache,
35
+ listSessionDeletionBackups,
32
36
  listSessions,
33
37
  loadDeletionStore,
34
38
  loadSessionStore,
@@ -5,6 +5,7 @@ import os from "node:os";
5
5
  import path from "node:path";
6
6
  import readline from "node:readline";
7
7
 
8
+ import { measurePath } from "../../storage/files.mjs";
8
9
  import {
9
10
  inspectJsonlMatches,
10
11
  readJsonlEntries,
@@ -42,6 +43,27 @@ function normalizeDisplayName(value) {
42
43
  return normalizeText(value);
43
44
  }
44
45
 
46
+ function cleanDerivedTitle(value) {
47
+ const original = normalizeDisplayName(value);
48
+
49
+ if (!original) {
50
+ return "";
51
+ }
52
+
53
+ let cleaned = original.replace(/^\[\d+\]\s+(?:user|assistant):\s*/u, "");
54
+ const nextRoleMarker = cleaned.search(/\s\[\d+\]\s+(?:user|assistant):\s*/u);
55
+
56
+ if (nextRoleMarker >= 0) {
57
+ cleaned = cleaned.slice(0, nextRoleMarker);
58
+ }
59
+
60
+ cleaned = normalizeDisplayName(
61
+ cleaned.replace(/\[([^\[\]]+?)\]\([^()]*?\)/gu, "$1"),
62
+ );
63
+
64
+ return cleaned && /[\p{L}\p{N}]/u.test(cleaned) ? cleaned : original;
65
+ }
66
+
45
67
  function toTimestampMs(value) {
46
68
  if (typeof value === "number" && Number.isFinite(value)) {
47
69
  return value;
@@ -368,7 +390,7 @@ function deriveDisplayName({
368
390
  if (sessionIndexEntry?.threadName && (!sqliteTitle || sqliteTitleLooksPrompt)) {
369
391
  return {
370
392
  source: "session_index",
371
- value: sessionIndexEntry.threadName,
393
+ value: cleanDerivedTitle(sessionIndexEntry.threadName),
372
394
  };
373
395
  }
374
396
 
@@ -378,21 +400,21 @@ function deriveDisplayName({
378
400
  ) {
379
401
  return {
380
402
  source: "transcript_thread_name",
381
- value: transcriptFallback.latestThreadName,
403
+ value: cleanDerivedTitle(transcriptFallback.latestThreadName),
382
404
  };
383
405
  }
384
406
 
385
407
  if (sqliteTitle) {
386
408
  return {
387
409
  source: "sqlite_title",
388
- value: sqliteTitle,
410
+ value: cleanDerivedTitle(sqliteTitle),
389
411
  };
390
412
  }
391
413
 
392
414
  if (sqliteFirstUserMessage) {
393
415
  return {
394
416
  source: "sqlite_first_user_message",
395
- value: sqliteFirstUserMessage,
417
+ value: cleanDerivedTitle(sqliteFirstUserMessage),
396
418
  };
397
419
  }
398
420
 
@@ -401,14 +423,14 @@ function deriveDisplayName({
401
423
  if (historyEntry?.text) {
402
424
  return {
403
425
  source: "history_first_user_message",
404
- value: historyEntry.text,
426
+ value: cleanDerivedTitle(historyEntry.text),
405
427
  };
406
428
  }
407
429
 
408
430
  if (transcriptFallback?.firstUserMessage) {
409
431
  return {
410
432
  source: "transcript_first_user_message",
411
- value: transcriptFallback.firstUserMessage,
433
+ value: cleanDerivedTitle(transcriptFallback.firstUserMessage),
412
434
  };
413
435
  }
414
436
 
@@ -452,7 +474,7 @@ const COMPATIBILITY_PROFILE = {
452
474
  id: "local-store-2026-07",
453
475
  builtFor: {
454
476
  chatgptDesktop: ["26.727.40816"],
455
- codexCli: ["0.144.1"],
477
+ codexCli: ["0.144.1", "0.146.0"],
456
478
  },
457
479
  };
458
480
 
@@ -895,10 +917,88 @@ export async function assertDeepCleanupSupported({ codexHome }) {
895
917
  const DEFAULT_PAGE_SIZE = 25;
896
918
  const MAX_PAGE_SIZE = 100;
897
919
  const OVERVIEW_STAT_BATCH_SIZE = 24;
920
+ const SESSION_SIZE_CACHE_TTL_MS = 45 * 1000;
921
+ const SESSION_SIZE_STAT_CONCURRENCY = 64;
898
922
  const SUPPORTING_THREAD_PREFIX = "The following is the Codex agent history whose request action you are assessing";
899
923
  const SESSION_CREATED_SQL = "coalesce(nullif(t.created_at_ms, 0), nullif(t.created_at, 0) * 1000, 0)";
900
924
  const SESSION_UPDATED_SQL = "coalesce(nullif(t.updated_at_ms, 0), nullif(t.updated_at, 0) * 1000, 0)";
901
925
  const SESSION_ACTIVITY_SQL = `coalesce(nullif(${SESSION_UPDATED_SQL}, 0), nullif(${SESSION_CREATED_SQL}, 0), 0)`;
926
+ const SESSION_SORTS = new Set(["created", "cwd", "name", "size", "updated"]);
927
+ const sessionSizeCache = new Map();
928
+
929
+ async function buildSessionSizeIndex(paths) {
930
+ const rows = queryRows(
931
+ paths.stateDatabasePath,
932
+ "select id, rollout_path from threads",
933
+ );
934
+ const sizes = new Map();
935
+ let nextIndex = 0;
936
+
937
+ const measureNext = async () => {
938
+ while (nextIndex < rows.length) {
939
+ const row = rows[nextIndex];
940
+ nextIndex += 1;
941
+ const id = String(row.id);
942
+
943
+ if (!row.rollout_path) {
944
+ sizes.set(id, null);
945
+ continue;
946
+ }
947
+
948
+ try {
949
+ const stats = await fs.stat(row.rollout_path);
950
+ sizes.set(id, stats.isFile() ? stats.size : null);
951
+ } catch {
952
+ sizes.set(id, null);
953
+ }
954
+ }
955
+ };
956
+
957
+ await Promise.all(
958
+ Array.from(
959
+ { length: Math.min(SESSION_SIZE_STAT_CONCURRENCY, rows.length) },
960
+ measureNext,
961
+ ),
962
+ );
963
+ return sizes;
964
+ }
965
+
966
+ async function getSessionSizeIndex(paths, { refresh = false } = {}) {
967
+ const cached = sessionSizeCache.get(paths.codexHome);
968
+
969
+ if (!refresh && cached?.expiresAtMs > Date.now()) {
970
+ return cached.promise;
971
+ }
972
+
973
+ const promise = buildSessionSizeIndex(paths).catch((error) => {
974
+ if (sessionSizeCache.get(paths.codexHome)?.promise === promise) {
975
+ sessionSizeCache.delete(paths.codexHome);
976
+ }
977
+ throw error;
978
+ });
979
+ sessionSizeCache.set(paths.codexHome, {
980
+ expiresAtMs: Date.now() + SESSION_SIZE_CACHE_TTL_MS,
981
+ promise,
982
+ });
983
+ return promise;
984
+ }
985
+
986
+ export function invalidateSessionCache({ codexHome }) {
987
+ sessionSizeCache.delete(getCodexPaths(codexHome).codexHome);
988
+ }
989
+
990
+ function compareSessionIdsBySize(leftId, rightId, sizes) {
991
+ const leftSize = sizes.get(leftId);
992
+ const rightSize = sizes.get(rightId);
993
+ const leftKnown = Number.isFinite(leftSize);
994
+ const rightKnown = Number.isFinite(rightSize);
995
+
996
+ if (leftKnown !== rightKnown) {
997
+ return leftKnown ? -1 : 1;
998
+ }
999
+
1000
+ return (rightSize ?? 0) - (leftSize ?? 0) || leftId.localeCompare(rightId);
1001
+ }
902
1002
 
903
1003
  function getSessionOrder(sort) {
904
1004
  const name = "lower(coalesce(nullif(trim(t.title), ''), nullif(trim(t.first_user_message), ''), t.id))";
@@ -1014,18 +1114,22 @@ async function formatPagedThreadRows(stateDatabasePath, threadRows) {
1014
1114
  const records = [];
1015
1115
 
1016
1116
  for (const threadRow of threadRows) {
1017
- const title = normalizeDisplayName(threadRow.title ?? "");
1018
- const firstUserMessage = getMeaningfulUserText(threadRow.first_user_message ?? "");
1117
+ const title = cleanDerivedTitle(threadRow.title ?? "");
1118
+ const firstUserMessage = cleanDerivedTitle(
1119
+ getMeaningfulUserText(threadRow.first_user_message ?? ""),
1120
+ );
1019
1121
  const displayName = title || firstUserMessage || `Untitled ${String(threadRow.id).slice(0, 8)}`;
1020
1122
  const rolloutPath = threadRow.rollout_path ?? "";
1021
1123
  let transcriptHeader = null;
1124
+ let transcriptBytes = null;
1022
1125
 
1023
1126
  if (rolloutPath) {
1024
- try {
1025
- transcriptHeader = await parseTranscriptHeader(rolloutPath);
1026
- } catch {
1027
- transcriptHeader = null;
1028
- }
1127
+ const [header, stats] = await Promise.all([
1128
+ parseTranscriptHeader(rolloutPath).catch(() => null),
1129
+ fs.stat(rolloutPath).catch(() => null),
1130
+ ]);
1131
+ transcriptHeader = header;
1132
+ transcriptBytes = stats?.isFile() ? stats.size : null;
1029
1133
  }
1030
1134
 
1031
1135
  const parentThreadId = threadRow.parent_thread_id ?? transcriptHeader?.parentThreadId ?? null;
@@ -1051,6 +1155,7 @@ async function formatPagedThreadRows(stateDatabasePath, threadRows) {
1051
1155
  rolloutPath,
1052
1156
  title: threadRow.title ?? "",
1053
1157
  titleSource: title ? "sqlite_title" : firstUserMessage ? "sqlite_first_user_message" : "fallback",
1158
+ transcriptBytes,
1054
1159
  updatedAtMs: toTimestampMs(threadRow.updated_at_ms),
1055
1160
  });
1056
1161
  }
@@ -1336,6 +1441,7 @@ export async function listSessions({
1336
1441
  includeSupporting = false,
1337
1442
  page = 1,
1338
1443
  pageSize = DEFAULT_PAGE_SIZE,
1444
+ refresh = false,
1339
1445
  search = "",
1340
1446
  sort = "updated",
1341
1447
  workspace,
@@ -1345,6 +1451,7 @@ export async function listSessions({
1345
1451
  ? Math.min(MAX_PAGE_SIZE, Math.max(1, Math.trunc(pageSize)))
1346
1452
  : DEFAULT_PAGE_SIZE;
1347
1453
  const requestedPage = Number.isFinite(page) ? Math.max(1, Math.trunc(page)) : 1;
1454
+ const resolvedSort = SESSION_SORTS.has(sort) ? sort : "updated";
1348
1455
  const conditions = getSessionConditions({
1349
1456
  archiveStatus,
1350
1457
  inactiveBeforeMs,
@@ -1353,6 +1460,42 @@ export async function listSessions({
1353
1460
  search,
1354
1461
  workspace,
1355
1462
  });
1463
+ if (resolvedSort === "size") {
1464
+ const sizes = await getSessionSizeIndex(paths, { refresh });
1465
+ const matchingIds = queryRows(
1466
+ paths.stateDatabasePath,
1467
+ `select t.id from threads t ${conditions.sql}`,
1468
+ conditions.parameters,
1469
+ ).map((row) => String(row.id));
1470
+ matchingIds.sort((left, right) => compareSessionIdsBySize(left, right, sizes));
1471
+ const total = matchingIds.length;
1472
+ const pageCount = Math.max(1, Math.ceil(total / boundedPageSize));
1473
+ const currentPage = Math.min(requestedPage, pageCount);
1474
+ const pageIds = matchingIds.slice(
1475
+ (currentPage - 1) * boundedPageSize,
1476
+ currentPage * boundedPageSize,
1477
+ );
1478
+ const threadRows = pageIds.length > 0
1479
+ ? queryRows(
1480
+ paths.stateDatabasePath,
1481
+ `select ${SESSION_COLUMNS}
1482
+ from threads t
1483
+ where t.id in (${placeholders(pageIds)})`,
1484
+ pageIds,
1485
+ )
1486
+ : [];
1487
+ const rowsById = new Map(threadRows.map((row) => [String(row.id), row]));
1488
+ const orderedRows = pageIds.map((id) => rowsById.get(id)).filter(Boolean);
1489
+
1490
+ return {
1491
+ page: currentPage,
1492
+ pageCount,
1493
+ pageSize: boundedPageSize,
1494
+ records: await formatPagedThreadRows(paths.stateDatabasePath, orderedRows),
1495
+ total,
1496
+ };
1497
+ }
1498
+
1356
1499
  const countRow = queryRows(
1357
1500
  paths.stateDatabasePath,
1358
1501
  `select count(*) as count from threads t ${conditions.sql}`,
@@ -1366,7 +1509,7 @@ export async function listSessions({
1366
1509
  `select ${SESSION_COLUMNS}
1367
1510
  from threads t
1368
1511
  ${conditions.sql}
1369
- order by ${getSessionOrder(sort)}
1512
+ order by ${getSessionOrder(resolvedSort)}
1370
1513
  limit ? offset ?`,
1371
1514
  [...conditions.parameters, boundedPageSize, (currentPage - 1) * boundedPageSize],
1372
1515
  );
@@ -1429,7 +1572,7 @@ async function measureTranscriptStorage(transcriptDirectories) {
1429
1572
  };
1430
1573
  }
1431
1574
 
1432
- export async function getSessionOverview({ codexHome }) {
1575
+ export async function getSessionOverview({ codexHome, refresh = false }) {
1433
1576
  const paths = getCodexPaths(codexHome);
1434
1577
  const supportingPattern = `${SUPPORTING_THREAD_PREFIX}%`;
1435
1578
  const overviewRow = queryRows(
@@ -1456,20 +1599,37 @@ export async function getSessionOverview({ codexHome }) {
1456
1599
  ) children on children.child_thread_id = t.id`,
1457
1600
  [supportingPattern, supportingPattern],
1458
1601
  )[0] ?? {};
1459
- const workspaceRows = queryRows(
1602
+ const workspaceSessionRows = queryRows(
1460
1603
  paths.stateDatabasePath,
1461
1604
  `select
1605
+ t.id,
1462
1606
  coalesce(t.cwd, '') as path,
1463
- count(*) as session_count,
1464
- max(${SESSION_ACTIVITY_SQL}) as last_activity_at_ms
1465
- from threads t
1466
- group by coalesce(t.cwd, '')
1467
- order by
1468
- case when coalesce(t.cwd, '') = '' then 1 else 0 end,
1469
- last_activity_at_ms desc,
1470
- lower(coalesce(t.cwd, '')) asc`,
1607
+ ${SESSION_ACTIVITY_SQL} as last_activity_at_ms
1608
+ from threads t`,
1471
1609
  );
1472
- const storage = await measureTranscriptStorage(paths.transcriptDirectories);
1610
+ const [sizes, storage] = await Promise.all([
1611
+ getSessionSizeIndex(paths, { refresh }),
1612
+ measureTranscriptStorage(paths.transcriptDirectories),
1613
+ ]);
1614
+ const workspaces = new Map();
1615
+
1616
+ for (const row of workspaceSessionRows) {
1617
+ const workspacePath = row.path ?? "";
1618
+ const current = workspaces.get(workspacePath) ?? {
1619
+ lastActivityAtMs: 0,
1620
+ path: workspacePath,
1621
+ sessionCount: 0,
1622
+ transcriptBytes: 0,
1623
+ };
1624
+ current.lastActivityAtMs = Math.max(
1625
+ current.lastActivityAtMs,
1626
+ toTimestampMs(row.last_activity_at_ms),
1627
+ );
1628
+ current.sessionCount += 1;
1629
+ const transcriptBytes = sizes.get(String(row.id));
1630
+ if (Number.isFinite(transcriptBytes)) current.transcriptBytes += transcriptBytes;
1631
+ workspaces.set(workspacePath, current);
1632
+ }
1473
1633
 
1474
1634
  return {
1475
1635
  activeSessionCount: Number(overviewRow.active_count ?? 0),
@@ -1480,11 +1640,10 @@ export async function getSessionOverview({ codexHome }) {
1480
1640
  subagentCount: Number(overviewRow.subagent_count ?? 0),
1481
1641
  supportingCount: Number(overviewRow.supporting_count ?? 0),
1482
1642
  unknownActivityCount: Number(overviewRow.unknown_activity_count ?? 0),
1483
- workspaces: workspaceRows.map((row) => ({
1484
- lastActivityAtMs: toTimestampMs(row.last_activity_at_ms),
1485
- path: row.path ?? "",
1486
- sessionCount: Number(row.session_count ?? 0),
1487
- })),
1643
+ workspaces: [...workspaces.values()].sort((left, right) =>
1644
+ Number(!left.path) - Number(!right.path)
1645
+ || right.lastActivityAtMs - left.lastActivityAtMs
1646
+ || left.path.localeCompare(right.path)),
1488
1647
  ...storage,
1489
1648
  };
1490
1649
  }
@@ -1545,6 +1704,12 @@ export function filterAndSortSessions({
1545
1704
  name: (left, right) =>
1546
1705
  left.displayName.localeCompare(right.displayName) ||
1547
1706
  right.updatedAtMs - left.updatedAtMs,
1707
+ size: (left, right) => {
1708
+ const leftKnown = Number.isFinite(left.transcriptBytes);
1709
+ const rightKnown = Number.isFinite(right.transcriptBytes);
1710
+ if (leftKnown !== rightKnown) return leftKnown ? -1 : 1;
1711
+ return (right.transcriptBytes ?? 0) - (left.transcriptBytes ?? 0);
1712
+ },
1548
1713
  updated: (left, right) =>
1549
1714
  right.updatedAtMs - left.updatedAtMs || left.displayName.localeCompare(right.displayName),
1550
1715
  };
@@ -1713,19 +1878,19 @@ export async function planSessionDeletion({ recordIds, store }) {
1713
1878
  const BACKUP_MINIMUM_RESERVE_BYTES = 1024 * 1024;
1714
1879
  const BACKUP_RESERVE_RATIO = 0.05;
1715
1880
 
1716
- function getBackupSourcePaths({ plan, store }) {
1881
+ function getBackupSourcePaths({ plan, scope, store }) {
1717
1882
  const databasePaths = [
1718
1883
  store.stateDatabasePath,
1719
1884
  store.hasLogsDatabase ? store.logsDatabasePath : null,
1720
- store.hasMemoryDatabase ? store.memoryDatabasePath : null,
1721
- store.hasGoalsDatabase ? store.goalsDatabasePath : null,
1885
+ scope === "deep" && store.hasMemoryDatabase ? store.memoryDatabasePath : null,
1886
+ scope === "deep" && store.hasGoalsDatabase ? store.goalsDatabasePath : null,
1722
1887
  ].filter(Boolean);
1723
1888
 
1724
1889
  return [
1725
1890
  store.historyPath,
1726
1891
  store.sessionIndexPath,
1727
- store.hasDesktopState ? store.desktopStatePath : null,
1728
- store.hasDesktopStateBackup ? store.desktopStateBackupPath : null,
1892
+ scope === "deep" && store.hasDesktopState ? store.desktopStatePath : null,
1893
+ scope === "deep" && store.hasDesktopStateBackup ? store.desktopStateBackupPath : null,
1729
1894
  ...plan.transcriptPaths,
1730
1895
  ...databasePaths.flatMap((databasePath) => [
1731
1896
  databasePath,
@@ -1811,9 +1976,9 @@ export async function fingerprintSessionDeletion({ plan, scope, store }) {
1811
1976
  return hash.digest("hex");
1812
1977
  }
1813
1978
 
1814
- async function estimateBackupBytes({ plan, store }) {
1979
+ async function estimateBackupBytes({ plan, scope, store }) {
1815
1980
  let sourceBytes = 0;
1816
- const uniquePaths = new Set(getBackupSourcePaths({ plan, store }));
1981
+ const uniquePaths = new Set(getBackupSourcePaths({ plan, scope, store }));
1817
1982
 
1818
1983
  for (const sourcePath of uniquePaths) {
1819
1984
  try {
@@ -1869,7 +2034,7 @@ function formatBytes(bytes) {
1869
2034
  return `${value.toFixed(value < 10 ? 1 : 0)} ${unit}`;
1870
2035
  }
1871
2036
 
1872
- export async function preflightSessionDeletion({ availableDiskBytes, plan, store }) {
2037
+ export async function preflightSessionDeletion({ availableDiskBytes, plan, scope = "deep", store }) {
1873
2038
  const requiredPaths = [store.stateDatabasePath, store.sessionIndexPath, store.historyPath];
1874
2039
  const missingRequiredPaths = [];
1875
2040
 
@@ -1889,7 +2054,7 @@ export async function preflightSessionDeletion({ availableDiskBytes, plan, store
1889
2054
  store.hasDesktopStateBackup ? readJsonFile(store.desktopStateBackupPath) : null,
1890
2055
  ]);
1891
2056
  const [desktopState, desktopStateBackup] = desktopStates;
1892
- const backupEstimate = await estimateBackupBytes({ plan, store });
2057
+ const backupEstimate = await estimateBackupBytes({ plan, scope, store });
1893
2058
  const diskCapacityBytes = availableDiskBytes ?? await getAvailableDiskBytes(store.codexHome);
1894
2059
 
1895
2060
  if (diskCapacityBytes < backupEstimate.estimatedBackupBytes) {
@@ -2079,7 +2244,7 @@ export async function executeSessionDeletion({
2079
2244
  });
2080
2245
  if (cancellationRequested(shouldCancel)) throw cleanupCancelled();
2081
2246
  const deletedIdSet = new Set(plan.ids);
2082
- const preflight = await preflightSessionDeletion({ plan, store });
2247
+ const preflight = await preflightSessionDeletion({ plan, scope, store });
2083
2248
  if (cancellationRequested(shouldCancel)) throw cleanupCancelled();
2084
2249
  const backupDirectory = await createOperationBackup({
2085
2250
  onProgress,
@@ -2241,6 +2406,55 @@ export async function deleteSessionDeletionBackup({ backupDirectory, codexHome }
2241
2406
  return { backupDirectory: resolvedBackupDirectory };
2242
2407
  }
2243
2408
 
2409
+ export async function listSessionDeletionBackups({ codexHome }) {
2410
+ const backupRoot = path.join(path.resolve(codexHome), "session-steward-backups");
2411
+ let entries;
2412
+
2413
+ try {
2414
+ entries = await fs.readdir(backupRoot, { withFileTypes: true });
2415
+ } catch (error) {
2416
+ if (error?.code === "ENOENT") return [];
2417
+ throw error;
2418
+ }
2419
+
2420
+ const backups = [];
2421
+
2422
+ for (const entry of entries) {
2423
+ if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
2424
+ const backupDirectory = path.join(backupRoot, entry.name);
2425
+ let operation = null;
2426
+
2427
+ try {
2428
+ operation = await readJsonFile(path.join(backupDirectory, "operation.json"));
2429
+ } catch (error) {
2430
+ if (!(error instanceof SyntaxError)) throw error;
2431
+ }
2432
+
2433
+ const [stats, measured] = await Promise.all([
2434
+ fs.stat(backupDirectory),
2435
+ measurePath(backupDirectory),
2436
+ ]);
2437
+ const restorable = operation?.version === 2 &&
2438
+ Array.isArray(operation.files) &&
2439
+ operation.files.length > 0;
2440
+
2441
+ backups.push({
2442
+ backupDirectory,
2443
+ bytes: measured.bytes,
2444
+ createdAtMs: toTimestampMs(operation?.createdAtMs) || stats.mtimeMs,
2445
+ fileCount: measured.fileCount,
2446
+ id: entry.name,
2447
+ itemCount: Array.isArray(operation?.files) ? operation.files.length : measured.fileCount,
2448
+ providerId: "codex",
2449
+ restorable,
2450
+ scope: operation?.scope === "core" || operation?.scope === "deep" ? operation.scope : null,
2451
+ sessionCount: Array.isArray(operation?.ids) ? operation.ids.length : null,
2452
+ });
2453
+ }
2454
+
2455
+ return backups.sort((left, right) => right.createdAtMs - left.createdAtMs || left.id.localeCompare(right.id));
2456
+ }
2457
+
2244
2458
  async function atomicCopyFile(sourcePath, destinationPath) {
2245
2459
  await fs.mkdir(path.dirname(destinationPath), { recursive: true });
2246
2460
  const temporaryPath = `${destinationPath}.session-steward-${process.pid}-${Date.now()}.tmp`;
@@ -2493,6 +2707,7 @@ export function formatSessionForJson(sessionRecord) {
2493
2707
  rolloutMissing: sessionRecord.rolloutMissing,
2494
2708
  rolloutPath: sessionRecord.rolloutPath,
2495
2709
  titleSource: sessionRecord.titleSource,
2710
+ transcriptBytes: sessionRecord.transcriptBytes,
2496
2711
  updatedAtMs: sessionRecord.updatedAtMs,
2497
2712
  };
2498
2713
  }
@@ -1,6 +1,7 @@
1
1
  import { codexProvider } from "./codex/index.mjs";
2
+ import { claudeCodeProvider } from "./claude-code/index.mjs";
2
3
 
3
- const providers = Object.freeze([codexProvider]);
4
+ const providers = Object.freeze([codexProvider, claudeCodeProvider]);
4
5
  const providersById = new Map(providers.map((provider) => [provider.id, provider]));
5
6
 
6
7
  export function getProvider(providerId) {