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.
- package/CHANGELOG.md +57 -0
- package/README.md +77 -21
- package/bin/session-steward-cli.mjs +37 -7
- package/bin/session-steward.mjs +3 -0
- package/dist/assets/index-CN9iax_v.css +2 -0
- package/dist/assets/index-fUX3qen0.js +9 -0
- package/dist/index.html +14 -3
- package/lib/cli.mjs +366 -59
- package/lib/providers/claude-code/index.mjs +7 -0
- package/lib/providers/claude-code/store.mjs +951 -0
- package/lib/providers/codex/index.mjs +4 -0
- package/lib/providers/codex/store.mjs +256 -41
- package/lib/providers/index.mjs +2 -1
- package/lib/server.mjs +147 -62
- package/lib/settings.mjs +39 -0
- package/lib/storage/files.mjs +39 -0
- package/package.json +9 -2
- package/dist/assets/index-kZ4XDVk-.js +0 -9
- package/dist/assets/index-pzaccjP4.css +0 -2
|
@@ -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 =
|
|
1018
|
-
const firstUserMessage =
|
|
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
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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(
|
|
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
|
|
1602
|
+
const workspaceSessionRows = queryRows(
|
|
1460
1603
|
paths.stateDatabasePath,
|
|
1461
1604
|
`select
|
|
1605
|
+
t.id,
|
|
1462
1606
|
coalesce(t.cwd, '') as path,
|
|
1463
|
-
|
|
1464
|
-
|
|
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
|
|
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:
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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
|
}
|
package/lib/providers/index.mjs
CHANGED
|
@@ -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) {
|