session-steward 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -106
- package/bin/session-steward-cli.mjs +12 -0
- package/bin/session-steward.mjs +23 -2
- package/dist/assets/index-kZ4XDVk-.js +9 -0
- package/dist/assets/index-pzaccjP4.css +2 -0
- package/dist/index.html +2 -2
- package/lib/cli.mjs +101 -5
- package/lib/providers/codex/index.mjs +4 -0
- package/lib/providers/codex/store.mjs +237 -39
- package/lib/server.mjs +181 -9
- package/package.json +12 -10
- package/dist/assets/index-DGVNvKX8.js +0 -9
- package/dist/assets/index-DTDZWQb9.css +0 -2
- package/docs/session-steward-overview.jpg +0 -0
|
@@ -139,6 +139,12 @@ async function pathExists(targetPath) {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
function isContainedPath(rootDirectory, candidatePath) {
|
|
143
|
+
if (!candidatePath) return false;
|
|
144
|
+
const relativePath = path.relative(path.resolve(rootDirectory), path.resolve(candidatePath));
|
|
145
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
146
|
+
}
|
|
147
|
+
|
|
142
148
|
async function readFirstLine(filePath) {
|
|
143
149
|
const stream = createReadStream(filePath, {
|
|
144
150
|
encoding: "utf8",
|
|
@@ -414,8 +420,11 @@ function deriveDisplayName({
|
|
|
414
420
|
|
|
415
421
|
function getCodexPaths(codexHomeInput) {
|
|
416
422
|
const codexHome = path.resolve(expandHome(codexHomeInput || "~/.codex"));
|
|
423
|
+
const archivedSessionsDirectory = path.join(codexHome, "archived_sessions");
|
|
424
|
+
const sessionsDirectory = path.join(codexHome, "sessions");
|
|
417
425
|
|
|
418
426
|
return {
|
|
427
|
+
archivedSessionsDirectory,
|
|
419
428
|
codexHome,
|
|
420
429
|
desktopStateBackupPath: path.join(codexHome, ".codex-global-state.json.bak"),
|
|
421
430
|
desktopStatePath: path.join(codexHome, ".codex-global-state.json"),
|
|
@@ -424,8 +433,9 @@ function getCodexPaths(codexHomeInput) {
|
|
|
424
433
|
logsDatabasePath: path.join(codexHome, "logs_2.sqlite"),
|
|
425
434
|
memoryDatabasePath: path.join(codexHome, "memories_1.sqlite"),
|
|
426
435
|
sessionIndexPath: path.join(codexHome, "session_index.jsonl"),
|
|
427
|
-
sessionsDirectory
|
|
436
|
+
sessionsDirectory,
|
|
428
437
|
stateDatabasePath: path.join(codexHome, "state_5.sqlite"),
|
|
438
|
+
transcriptDirectories: [sessionsDirectory, archivedSessionsDirectory],
|
|
429
439
|
};
|
|
430
440
|
}
|
|
431
441
|
|
|
@@ -584,21 +594,7 @@ export async function loadSessionStore({ codexHome }) {
|
|
|
584
594
|
from thread_spawn_edges
|
|
585
595
|
`,
|
|
586
596
|
);
|
|
587
|
-
const transcriptHeaders =
|
|
588
|
-
|
|
589
|
-
for await (const transcriptFile of findTranscriptFiles(paths.sessionsDirectory)) {
|
|
590
|
-
try {
|
|
591
|
-
const header = await parseTranscriptHeader(transcriptFile);
|
|
592
|
-
|
|
593
|
-
const current = header?.id ? transcriptHeaders.get(header.id) : null;
|
|
594
|
-
|
|
595
|
-
if (header?.id && (!current || header.filePath > current.filePath)) {
|
|
596
|
-
transcriptHeaders.set(header.id, header);
|
|
597
|
-
}
|
|
598
|
-
} catch {
|
|
599
|
-
continue;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
597
|
+
const transcriptHeaders = await indexTranscriptHeaders(paths.transcriptDirectories);
|
|
602
598
|
|
|
603
599
|
const discoveryIds = new Set(threadRows.map((threadRow) => String(threadRow.id)));
|
|
604
600
|
|
|
@@ -698,7 +694,7 @@ export async function loadSessionStore({ codexHome }) {
|
|
|
698
694
|
const record = {
|
|
699
695
|
agentNickname: transcriptHeader.agentNickname,
|
|
700
696
|
agentRole: transcriptHeader.agentRole,
|
|
701
|
-
archived:
|
|
697
|
+
archived: isContainedPath(paths.archivedSessionsDirectory, transcriptHeader.filePath),
|
|
702
698
|
childThreadIds: childIdsByParentId.get(transcriptId) ?? [],
|
|
703
699
|
createdAtMs: transcriptHeader.timestampMs,
|
|
704
700
|
cwd: transcriptHeader.cwd,
|
|
@@ -898,25 +894,40 @@ export async function assertDeepCleanupSupported({ codexHome }) {
|
|
|
898
894
|
|
|
899
895
|
const DEFAULT_PAGE_SIZE = 25;
|
|
900
896
|
const MAX_PAGE_SIZE = 100;
|
|
897
|
+
const OVERVIEW_STAT_BATCH_SIZE = 24;
|
|
901
898
|
const SUPPORTING_THREAD_PREFIX = "The following is the Codex agent history whose request action you are assessing";
|
|
899
|
+
const SESSION_CREATED_SQL = "coalesce(nullif(t.created_at_ms, 0), nullif(t.created_at, 0) * 1000, 0)";
|
|
900
|
+
const SESSION_UPDATED_SQL = "coalesce(nullif(t.updated_at_ms, 0), nullif(t.updated_at, 0) * 1000, 0)";
|
|
901
|
+
const SESSION_ACTIVITY_SQL = `coalesce(nullif(${SESSION_UPDATED_SQL}, 0), nullif(${SESSION_CREATED_SQL}, 0), 0)`;
|
|
902
902
|
|
|
903
903
|
function getSessionOrder(sort) {
|
|
904
|
-
const updated = "coalesce(t.updated_at_ms, t.updated_at * 1000, 0)";
|
|
905
|
-
const created = "coalesce(t.created_at_ms, t.created_at * 1000, 0)";
|
|
906
904
|
const name = "lower(coalesce(nullif(trim(t.title), ''), nullif(trim(t.first_user_message), ''), t.id))";
|
|
907
905
|
|
|
908
906
|
return {
|
|
909
|
-
created: `${
|
|
910
|
-
cwd: `lower(coalesce(t.cwd, '')) asc, ${
|
|
911
|
-
name: `${name} asc, ${
|
|
912
|
-
updated: `${
|
|
913
|
-
}[sort] ?? `${
|
|
907
|
+
created: `${SESSION_CREATED_SQL} desc, t.id asc`,
|
|
908
|
+
cwd: `lower(coalesce(t.cwd, '')) asc, ${SESSION_ACTIVITY_SQL} desc, t.id asc`,
|
|
909
|
+
name: `${name} asc, ${SESSION_ACTIVITY_SQL} desc, t.id asc`,
|
|
910
|
+
updated: `${SESSION_ACTIVITY_SQL} desc, t.id asc`,
|
|
911
|
+
}[sort] ?? `${SESSION_ACTIVITY_SQL} desc, t.id asc`;
|
|
914
912
|
}
|
|
915
913
|
|
|
916
|
-
function getSessionConditions({
|
|
914
|
+
function getSessionConditions({
|
|
915
|
+
archiveStatus,
|
|
916
|
+
inactiveBeforeMs,
|
|
917
|
+
includeInternals,
|
|
918
|
+
includeSupporting,
|
|
919
|
+
search,
|
|
920
|
+
workspace,
|
|
921
|
+
}) {
|
|
917
922
|
const conditions = [];
|
|
918
923
|
const parameters = [];
|
|
919
924
|
|
|
925
|
+
if (archiveStatus === "active") {
|
|
926
|
+
conditions.push("coalesce(t.archived, 0) = 0");
|
|
927
|
+
} else if (archiveStatus === "archived") {
|
|
928
|
+
conditions.push("coalesce(t.archived, 0) <> 0");
|
|
929
|
+
}
|
|
930
|
+
|
|
920
931
|
if (!includeInternals) {
|
|
921
932
|
conditions.push(`not exists (
|
|
922
933
|
select 1 from thread_spawn_edges edge where edge.child_thread_id = t.id
|
|
@@ -928,6 +939,16 @@ function getSessionConditions({ includeInternals, includeSupporting, search }) {
|
|
|
928
939
|
parameters.push(`${SUPPORTING_THREAD_PREFIX}%`);
|
|
929
940
|
}
|
|
930
941
|
|
|
942
|
+
if (Number.isFinite(inactiveBeforeMs) && inactiveBeforeMs > 0) {
|
|
943
|
+
conditions.push(`${SESSION_ACTIVITY_SQL} > 0 and ${SESSION_ACTIVITY_SQL} <= ?`);
|
|
944
|
+
parameters.push(Math.trunc(inactiveBeforeMs));
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (typeof workspace === "string") {
|
|
948
|
+
conditions.push("coalesce(t.cwd, '') = ?");
|
|
949
|
+
parameters.push(workspace);
|
|
950
|
+
}
|
|
951
|
+
|
|
931
952
|
const normalizedSearch = normalizeText(search).toLowerCase();
|
|
932
953
|
|
|
933
954
|
if (normalizedSearch) {
|
|
@@ -957,8 +978,8 @@ const SESSION_COLUMNS = `
|
|
|
957
978
|
t.agent_role,
|
|
958
979
|
t.archived,
|
|
959
980
|
t.is_pinned,
|
|
960
|
-
|
|
961
|
-
|
|
981
|
+
${SESSION_CREATED_SQL} as created_at_ms,
|
|
982
|
+
${SESSION_ACTIVITY_SQL} as updated_at_ms,
|
|
962
983
|
(
|
|
963
984
|
select edge.parent_thread_id
|
|
964
985
|
from thread_spawn_edges edge
|
|
@@ -1061,19 +1082,21 @@ async function getStoreAvailability(paths) {
|
|
|
1061
1082
|
};
|
|
1062
1083
|
}
|
|
1063
1084
|
|
|
1064
|
-
async function indexTranscriptHeaders(
|
|
1085
|
+
async function indexTranscriptHeaders(transcriptDirectories) {
|
|
1065
1086
|
const headersById = new Map();
|
|
1066
1087
|
|
|
1067
|
-
for
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1088
|
+
for (const transcriptDirectory of transcriptDirectories) {
|
|
1089
|
+
for await (const transcriptFile of findTranscriptFiles(transcriptDirectory)) {
|
|
1090
|
+
try {
|
|
1091
|
+
const header = await parseTranscriptHeader(transcriptFile);
|
|
1092
|
+
const current = header?.id ? headersById.get(String(header.id)) : null;
|
|
1071
1093
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1094
|
+
if (header?.id && (!current || header.filePath > current.filePath)) {
|
|
1095
|
+
headersById.set(String(header.id), { ...header, id: String(header.id) });
|
|
1096
|
+
}
|
|
1097
|
+
} catch {
|
|
1098
|
+
continue;
|
|
1074
1099
|
}
|
|
1075
|
-
} catch {
|
|
1076
|
-
continue;
|
|
1077
1100
|
}
|
|
1078
1101
|
}
|
|
1079
1102
|
|
|
@@ -1126,7 +1149,7 @@ export async function loadDeletionStore({ codexHome, recordIds }) {
|
|
|
1126
1149
|
|
|
1127
1150
|
const [availability, transcriptHeaders] = await Promise.all([
|
|
1128
1151
|
getStoreAvailability(paths),
|
|
1129
|
-
indexTranscriptHeaders(paths.
|
|
1152
|
+
indexTranscriptHeaders(paths.transcriptDirectories),
|
|
1130
1153
|
]);
|
|
1131
1154
|
const transcriptChildrenByParentId = getTranscriptChildrenByParentId(transcriptHeaders);
|
|
1132
1155
|
const pendingIds = [...selectedIds];
|
|
@@ -1218,7 +1241,7 @@ export async function loadDeletionStore({ codexHome, recordIds }) {
|
|
|
1218
1241
|
recordsById.set(id, {
|
|
1219
1242
|
agentNickname: header.agentNickname,
|
|
1220
1243
|
agentRole: header.agentRole,
|
|
1221
|
-
archived:
|
|
1244
|
+
archived: isContainedPath(paths.archivedSessionsDirectory, header.filePath),
|
|
1222
1245
|
childThreadIds: [...(childIdsByParentId.get(id) ?? [])].sort(),
|
|
1223
1246
|
createdAtMs: header.timestampMs,
|
|
1224
1247
|
cwd: header.cwd,
|
|
@@ -1306,20 +1329,30 @@ export async function loadDeletionStore({ codexHome, recordIds }) {
|
|
|
1306
1329
|
}
|
|
1307
1330
|
|
|
1308
1331
|
export async function listSessions({
|
|
1332
|
+
archiveStatus = "all",
|
|
1309
1333
|
codexHome,
|
|
1334
|
+
inactiveBeforeMs = null,
|
|
1310
1335
|
includeInternals = false,
|
|
1311
1336
|
includeSupporting = false,
|
|
1312
1337
|
page = 1,
|
|
1313
1338
|
pageSize = DEFAULT_PAGE_SIZE,
|
|
1314
1339
|
search = "",
|
|
1315
1340
|
sort = "updated",
|
|
1341
|
+
workspace,
|
|
1316
1342
|
}) {
|
|
1317
1343
|
const paths = getCodexPaths(codexHome);
|
|
1318
1344
|
const boundedPageSize = Number.isFinite(pageSize)
|
|
1319
1345
|
? Math.min(MAX_PAGE_SIZE, Math.max(1, Math.trunc(pageSize)))
|
|
1320
1346
|
: DEFAULT_PAGE_SIZE;
|
|
1321
1347
|
const requestedPage = Number.isFinite(page) ? Math.max(1, Math.trunc(page)) : 1;
|
|
1322
|
-
const conditions = getSessionConditions({
|
|
1348
|
+
const conditions = getSessionConditions({
|
|
1349
|
+
archiveStatus,
|
|
1350
|
+
inactiveBeforeMs,
|
|
1351
|
+
includeInternals,
|
|
1352
|
+
includeSupporting,
|
|
1353
|
+
search,
|
|
1354
|
+
workspace,
|
|
1355
|
+
});
|
|
1323
1356
|
const countRow = queryRows(
|
|
1324
1357
|
paths.stateDatabasePath,
|
|
1325
1358
|
`select count(*) as count from threads t ${conditions.sql}`,
|
|
@@ -1347,6 +1380,115 @@ export async function listSessions({
|
|
|
1347
1380
|
};
|
|
1348
1381
|
}
|
|
1349
1382
|
|
|
1383
|
+
async function measureTranscriptStorage(transcriptDirectories) {
|
|
1384
|
+
let transcriptBytes = 0;
|
|
1385
|
+
let transcriptFileCount = 0;
|
|
1386
|
+
let unreadableFileCount = 0;
|
|
1387
|
+
let pendingPaths = [];
|
|
1388
|
+
|
|
1389
|
+
const measurePendingPaths = async () => {
|
|
1390
|
+
const pathsToMeasure = pendingPaths;
|
|
1391
|
+
pendingPaths = [];
|
|
1392
|
+
const results = await Promise.all(pathsToMeasure.map(async (filePath) => {
|
|
1393
|
+
try {
|
|
1394
|
+
return await fs.stat(filePath);
|
|
1395
|
+
} catch {
|
|
1396
|
+
return null;
|
|
1397
|
+
}
|
|
1398
|
+
}));
|
|
1399
|
+
|
|
1400
|
+
for (const stats of results) {
|
|
1401
|
+
if (!stats?.isFile()) {
|
|
1402
|
+
unreadableFileCount += 1;
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
transcriptBytes += stats.size;
|
|
1407
|
+
transcriptFileCount += 1;
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
|
|
1411
|
+
for (const transcriptDirectory of transcriptDirectories) {
|
|
1412
|
+
for await (const transcriptPath of findTranscriptFiles(transcriptDirectory)) {
|
|
1413
|
+
pendingPaths.push(transcriptPath);
|
|
1414
|
+
|
|
1415
|
+
if (pendingPaths.length >= OVERVIEW_STAT_BATCH_SIZE) {
|
|
1416
|
+
await measurePendingPaths();
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
if (pendingPaths.length > 0) {
|
|
1422
|
+
await measurePendingPaths();
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
return {
|
|
1426
|
+
transcriptBytes,
|
|
1427
|
+
transcriptFileCount,
|
|
1428
|
+
unreadableFileCount,
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
export async function getSessionOverview({ codexHome }) {
|
|
1433
|
+
const paths = getCodexPaths(codexHome);
|
|
1434
|
+
const supportingPattern = `${SUPPORTING_THREAD_PREFIX}%`;
|
|
1435
|
+
const overviewRow = queryRows(
|
|
1436
|
+
paths.stateDatabasePath,
|
|
1437
|
+
`select
|
|
1438
|
+
count(*) as total,
|
|
1439
|
+
coalesce(sum(case when coalesce(t.archived, 0) <> 0 then 1 else 0 end), 0) as archived_count,
|
|
1440
|
+
coalesce(sum(case when coalesce(t.archived, 0) = 0 then 1 else 0 end), 0) as active_count,
|
|
1441
|
+
count(children.child_thread_id) as subagent_count,
|
|
1442
|
+
coalesce(sum(case
|
|
1443
|
+
when children.child_thread_id is null
|
|
1444
|
+
and not (coalesce(nullif(trim(t.title), ''), nullif(trim(t.first_user_message), ''), '') like ?)
|
|
1445
|
+
then 1
|
|
1446
|
+
else 0
|
|
1447
|
+
end), 0) as primary_session_count,
|
|
1448
|
+
coalesce(sum(case
|
|
1449
|
+
when coalesce(nullif(trim(t.title), ''), nullif(trim(t.first_user_message), ''), '') like ? then 1
|
|
1450
|
+
else 0
|
|
1451
|
+
end), 0) as supporting_count,
|
|
1452
|
+
coalesce(sum(case when ${SESSION_ACTIVITY_SQL} = 0 then 1 else 0 end), 0) as unknown_activity_count
|
|
1453
|
+
from threads t
|
|
1454
|
+
left join (
|
|
1455
|
+
select distinct child_thread_id from thread_spawn_edges
|
|
1456
|
+
) children on children.child_thread_id = t.id`,
|
|
1457
|
+
[supportingPattern, supportingPattern],
|
|
1458
|
+
)[0] ?? {};
|
|
1459
|
+
const workspaceRows = queryRows(
|
|
1460
|
+
paths.stateDatabasePath,
|
|
1461
|
+
`select
|
|
1462
|
+
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`,
|
|
1471
|
+
);
|
|
1472
|
+
const storage = await measureTranscriptStorage(paths.transcriptDirectories);
|
|
1473
|
+
|
|
1474
|
+
return {
|
|
1475
|
+
activeSessionCount: Number(overviewRow.active_count ?? 0),
|
|
1476
|
+
archivedSessionCount: Number(overviewRow.archived_count ?? 0),
|
|
1477
|
+
calculatedAtMs: Date.now(),
|
|
1478
|
+
primarySessionCount: Number(overviewRow.primary_session_count ?? 0),
|
|
1479
|
+
sessionCount: Number(overviewRow.total ?? 0),
|
|
1480
|
+
subagentCount: Number(overviewRow.subagent_count ?? 0),
|
|
1481
|
+
supportingCount: Number(overviewRow.supporting_count ?? 0),
|
|
1482
|
+
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
|
+
})),
|
|
1488
|
+
...storage,
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1350
1492
|
export async function getSessionRecord({ codexHome, id }) {
|
|
1351
1493
|
const paths = getCodexPaths(codexHome);
|
|
1352
1494
|
const rows = queryRows(
|
|
@@ -1359,6 +1501,7 @@ export async function getSessionRecord({ codexHome, id }) {
|
|
|
1359
1501
|
}
|
|
1360
1502
|
|
|
1361
1503
|
export function filterAndSortSessions({
|
|
1504
|
+
archiveStatus = "all",
|
|
1362
1505
|
includeInternals,
|
|
1363
1506
|
records,
|
|
1364
1507
|
search,
|
|
@@ -1366,6 +1509,14 @@ export function filterAndSortSessions({
|
|
|
1366
1509
|
}) {
|
|
1367
1510
|
const normalizedSearch = normalizeText(search).toLowerCase();
|
|
1368
1511
|
const filteredRecords = records.filter((record) => {
|
|
1512
|
+
if (archiveStatus === "active" && record.archived) {
|
|
1513
|
+
return false;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
if (archiveStatus === "archived" && !record.archived) {
|
|
1517
|
+
return false;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1369
1520
|
if (!includeInternals && record.isSubagent) {
|
|
1370
1521
|
return false;
|
|
1371
1522
|
}
|
|
@@ -1477,6 +1628,33 @@ export async function planSessionDeletion({ recordIds, store }) {
|
|
|
1477
1628
|
.map((record) => record.rolloutPath)
|
|
1478
1629
|
.filter(Boolean);
|
|
1479
1630
|
const deletionIdSet = new Set(deletionIds);
|
|
1631
|
+
const requestedIdSet = new Set(recordIds);
|
|
1632
|
+
const newestLinkedActivityAtMs = selectedRecords.reduce((newest, record) => {
|
|
1633
|
+
if (requestedIdSet.has(record.id)) {
|
|
1634
|
+
return newest;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
return Math.max(newest, record.updatedAtMs ?? 0);
|
|
1638
|
+
}, 0);
|
|
1639
|
+
let transcriptBytes = 0;
|
|
1640
|
+
let transcriptFileCount = 0;
|
|
1641
|
+
|
|
1642
|
+
for (const transcriptBatch of batches([...new Set(transcriptPaths)], OVERVIEW_STAT_BATCH_SIZE)) {
|
|
1643
|
+
const results = await Promise.all(transcriptBatch.map(async (transcriptPath) => {
|
|
1644
|
+
try {
|
|
1645
|
+
return await fs.stat(transcriptPath);
|
|
1646
|
+
} catch (error) {
|
|
1647
|
+
if (error?.code === "ENOENT") return null;
|
|
1648
|
+
throw error;
|
|
1649
|
+
}
|
|
1650
|
+
}));
|
|
1651
|
+
|
|
1652
|
+
for (const stats of results) {
|
|
1653
|
+
if (!stats?.isFile()) continue;
|
|
1654
|
+
transcriptBytes += stats.size;
|
|
1655
|
+
transcriptFileCount += 1;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1480
1658
|
const [historyMatches, sessionIndexMatches] = await Promise.all([
|
|
1481
1659
|
inspectJsonlMatches(
|
|
1482
1660
|
store.historyPath,
|
|
@@ -1522,9 +1700,12 @@ export async function planSessionDeletion({ recordIds, store }) {
|
|
|
1522
1700
|
logRowCount,
|
|
1523
1701
|
memoryRowCount,
|
|
1524
1702
|
missingTranscriptPaths,
|
|
1703
|
+
newestLinkedActivityAtMs,
|
|
1525
1704
|
records: selectedRecords,
|
|
1526
1705
|
sessionIndexMatchCount: sessionIndexMatches.count,
|
|
1527
1706
|
spawnEdgeCount,
|
|
1707
|
+
transcriptBytes,
|
|
1708
|
+
transcriptFileCount,
|
|
1528
1709
|
transcriptPaths,
|
|
1529
1710
|
};
|
|
1530
1711
|
}
|
|
@@ -2043,6 +2224,23 @@ function resolveContainedPath(rootDirectory, candidatePath) {
|
|
|
2043
2224
|
return resolved;
|
|
2044
2225
|
}
|
|
2045
2226
|
|
|
2227
|
+
export async function deleteSessionDeletionBackup({ backupDirectory, codexHome }) {
|
|
2228
|
+
const backupRoot = path.join(codexHome, "session-steward-backups");
|
|
2229
|
+
const resolvedBackupDirectory = resolveContainedPath(backupRoot, backupDirectory);
|
|
2230
|
+
|
|
2231
|
+
if (resolvedBackupDirectory === path.resolve(backupRoot)) {
|
|
2232
|
+
throw new Error("Choose one recovery backup to delete.");
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
await fs.rm(resolvedBackupDirectory, {
|
|
2236
|
+
force: true,
|
|
2237
|
+
maxRetries: 3,
|
|
2238
|
+
recursive: true,
|
|
2239
|
+
retryDelay: 25,
|
|
2240
|
+
});
|
|
2241
|
+
return { backupDirectory: resolvedBackupDirectory };
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2046
2244
|
async function atomicCopyFile(sourcePath, destinationPath) {
|
|
2047
2245
|
await fs.mkdir(path.dirname(destinationPath), { recursive: true });
|
|
2048
2246
|
const temporaryPath = `${destinationPath}.session-steward-${process.pid}-${Date.now()}.tmp`;
|