session-steward 0.5.2 → 0.7.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 +20 -0
- package/README.md +5 -3
- package/bin/session-steward.mjs +8 -7
- package/dist/assets/index-BOACkzUI.js +9 -0
- package/dist/assets/{index-Cn_JrqDr.css → index-DFAWGcgb.css} +1 -1
- package/dist/index.html +2 -2
- package/lib/cli.mjs +17 -0
- package/lib/platform.mjs +178 -0
- package/lib/providers/claude-code/events.mjs +49 -0
- package/lib/providers/claude-code/store.mjs +14 -14
- package/lib/providers/codex/database-families.mjs +18 -2
- package/lib/providers/codex/events.mjs +37 -0
- package/lib/providers/codex/store.mjs +104 -16
- package/lib/server.mjs +7 -1
- package/lib/session-events.mjs +72 -0
- package/lib/settings.mjs +7 -24
- package/lib/storage/jsonl.mjs +10 -1
- package/package.json +15 -17
- package/dist/assets/index-6OPqaZRp.js +0 -9
|
@@ -2,10 +2,14 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
2
2
|
import { once } from "node:events";
|
|
3
3
|
import { createReadStream, createWriteStream } from "node:fs";
|
|
4
4
|
import { promises as fs } from "node:fs";
|
|
5
|
-
import os from "node:os";
|
|
6
5
|
import path from "node:path";
|
|
7
6
|
import { finished, pipeline } from "node:stream/promises";
|
|
8
7
|
|
|
8
|
+
import {
|
|
9
|
+
expandHomePath,
|
|
10
|
+
getClaudeDesktopDataHome,
|
|
11
|
+
invalidateClaudeDesktopDataHome,
|
|
12
|
+
} from "../../platform.mjs";
|
|
9
13
|
import { measurePath } from "../../storage/files.mjs";
|
|
10
14
|
import { readJsonlEntries, rewriteJsonlFile } from "../../storage/jsonl.mjs";
|
|
11
15
|
|
|
@@ -13,8 +17,8 @@ const PROVIDER_ID = "claude-code";
|
|
|
13
17
|
const COMPATIBILITY_PROFILE = Object.freeze({
|
|
14
18
|
id: "claude-local-store-2026-08",
|
|
15
19
|
builtFor: {
|
|
16
|
-
claudeCli: ["2.1.199", "2.1.220", "2.1.228"],
|
|
17
|
-
claudeDesktop: ["1.24012.9", "1.28929.0"],
|
|
20
|
+
claudeCli: ["2.1.199", "2.1.220", "2.1.228", "2.1.237"],
|
|
21
|
+
claudeDesktop: ["1.24012.9", "1.28929.0", "1.32885.1"],
|
|
18
22
|
},
|
|
19
23
|
});
|
|
20
24
|
const SUPPORTED_ENTRYPOINTS = new Set(["cli", "claude-desktop"]);
|
|
@@ -34,19 +38,11 @@ const SESSION_SORTS = new Set(["created", "cwd", "name", "size", "updated"]);
|
|
|
34
38
|
const discoveryCache = new Map();
|
|
35
39
|
const transcriptActivityCache = new Map();
|
|
36
40
|
|
|
37
|
-
function expandHome(value) {
|
|
38
|
-
if (value === "~") return os.homedir();
|
|
39
|
-
if (value?.startsWith("~/")) return path.join(os.homedir(), value.slice(2));
|
|
40
|
-
return value;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
41
|
function getPaths(claudeHomeInput, desktopDataHomeInput) {
|
|
44
|
-
const claudeHome = path.resolve(
|
|
42
|
+
const claudeHome = path.resolve(expandHomePath(claudeHomeInput || process.env.CLAUDE_CONFIG_DIR || "~/.claude"));
|
|
45
43
|
const desktopDataHome = desktopDataHomeInput
|
|
46
|
-
? path.resolve(
|
|
47
|
-
:
|
|
48
|
-
? path.join(os.homedir(), "Library", "Application Support", "Claude")
|
|
49
|
-
: null;
|
|
44
|
+
? path.resolve(expandHomePath(desktopDataHomeInput))
|
|
45
|
+
: getClaudeDesktopDataHome();
|
|
50
46
|
return {
|
|
51
47
|
backupRoot: path.join(claudeHome, "session-steward-backups"),
|
|
52
48
|
claudeHome,
|
|
@@ -401,6 +397,9 @@ async function discover(claudeHome, desktopDataHome) {
|
|
|
401
397
|
}
|
|
402
398
|
|
|
403
399
|
async function discoverCached(claudeHome, desktopDataHome, { refresh = false } = {}) {
|
|
400
|
+
if (refresh && !desktopDataHome) {
|
|
401
|
+
invalidateClaudeDesktopDataHome();
|
|
402
|
+
}
|
|
404
403
|
const paths = getPaths(claudeHome, desktopDataHome);
|
|
405
404
|
const key = `${paths.claudeHome}\0${paths.desktopDataHome || ""}`;
|
|
406
405
|
const cached = discoveryCache.get(key);
|
|
@@ -416,6 +415,7 @@ async function discoverCached(claudeHome, desktopDataHome, { refresh = false } =
|
|
|
416
415
|
export function invalidateSessionCache({ claudeHome, desktopDataHome }) {
|
|
417
416
|
const paths = getPaths(claudeHome, desktopDataHome);
|
|
418
417
|
discoveryCache.delete(`${paths.claudeHome}\0${paths.desktopDataHome || ""}`);
|
|
418
|
+
invalidateClaudeDesktopDataHome();
|
|
419
419
|
}
|
|
420
420
|
|
|
421
421
|
function filterRecords(records, options) {
|
|
@@ -8,8 +8,8 @@ const CACHE_TTL_MS = 2_000;
|
|
|
8
8
|
export const CODEX_DATABASE_PROFILE = Object.freeze({
|
|
9
9
|
id: "codex-local-store-2026-08",
|
|
10
10
|
builtFor: {
|
|
11
|
-
chatgptDesktop: ["26.727.40816", "26.803.61601"],
|
|
12
|
-
codexCli: ["0.144.1", "0.146.0", "0.147.0"],
|
|
11
|
+
chatgptDesktop: ["26.727.40816", "26.803.61601", "26.818.21641", "26.818.22352"],
|
|
12
|
+
codexCli: ["0.144.1", "0.146.0", "0.147.0", "0.148.0"],
|
|
13
13
|
},
|
|
14
14
|
});
|
|
15
15
|
|
|
@@ -41,6 +41,22 @@ const SCHEMA_REQUIREMENTS = Object.freeze({
|
|
|
41
41
|
{ name: "thread_goal_continuation_deferrals", requiredColumns: ["thread_id"] },
|
|
42
42
|
],
|
|
43
43
|
},
|
|
44
|
+
queue: {
|
|
45
|
+
fallback: "queue_1.sqlite",
|
|
46
|
+
pattern: /^queue_(\d+)\.sqlite$/u,
|
|
47
|
+
required: false,
|
|
48
|
+
tables: [{ name: "queued_items", requiredColumns: ["thread_id"] }],
|
|
49
|
+
},
|
|
50
|
+
threadHistory: {
|
|
51
|
+
fallback: "thread_history_1.sqlite",
|
|
52
|
+
pattern: /^thread_history_(\d+)\.sqlite$/u,
|
|
53
|
+
required: false,
|
|
54
|
+
tables: [
|
|
55
|
+
{ name: "thread_items", requiredColumns: ["thread_id"] },
|
|
56
|
+
{ name: "thread_turns", requiredColumns: ["thread_id"] },
|
|
57
|
+
{ name: "thread_history_projection_state", requiredColumns: ["thread_id"] },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
44
60
|
});
|
|
45
61
|
|
|
46
62
|
const resolutionCache = new Map();
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createSessionEvent,
|
|
3
|
+
createSessionEventComposition,
|
|
3
4
|
createSessionEventCoverage,
|
|
4
5
|
createSessionEventHeader,
|
|
6
|
+
createSessionEventSummary,
|
|
5
7
|
createSessionEventsResult,
|
|
8
|
+
finalizeSessionEventComposition,
|
|
6
9
|
SESSION_EVENT_KIND,
|
|
7
10
|
SESSION_EVENT_REASON,
|
|
8
11
|
} from "../../session-events.mjs";
|
|
@@ -53,6 +56,30 @@ function createDuplicateTextTracker() {
|
|
|
53
56
|
};
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
const COMPOSITION_TOOL_OUTPUT = new Set([
|
|
60
|
+
"custom_tool_call_output",
|
|
61
|
+
"exec_command_end",
|
|
62
|
+
"function_call_output",
|
|
63
|
+
"mcp_tool_call_end",
|
|
64
|
+
"web_search_end",
|
|
65
|
+
]);
|
|
66
|
+
const COMPOSITION_MESSAGES = new Set(["agent_message", "message", "user_message"]);
|
|
67
|
+
const COMPOSITION_EDITS = new Set(["patch_apply_begin", "patch_apply_end"]);
|
|
68
|
+
const COMPOSITION_REASONING = new Set(["agent_reasoning", "reasoning"]);
|
|
69
|
+
|
|
70
|
+
function compositionSegment(parsed) {
|
|
71
|
+
const type = typeof parsed?.type === "string" ? parsed.type : "";
|
|
72
|
+
const payloadType = typeof parsed?.payload?.type === "string" ? parsed.payload.type : type;
|
|
73
|
+
if (type === "compacted" || payloadType === "compacted" || payloadType === "context_compacted") {
|
|
74
|
+
return "compaction";
|
|
75
|
+
}
|
|
76
|
+
if (COMPOSITION_TOOL_OUTPUT.has(payloadType)) return "toolOutput";
|
|
77
|
+
if (COMPOSITION_EDITS.has(payloadType)) return "edits";
|
|
78
|
+
if (COMPOSITION_REASONING.has(payloadType)) return "reasoning";
|
|
79
|
+
if (COMPOSITION_MESSAGES.has(payloadType)) return "messages";
|
|
80
|
+
return "other";
|
|
81
|
+
}
|
|
82
|
+
|
|
56
83
|
function asTimestamp(value) {
|
|
57
84
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
58
85
|
if (typeof value !== "string") return null;
|
|
@@ -339,6 +366,8 @@ export async function readSessionEvents({
|
|
|
339
366
|
}
|
|
340
367
|
|
|
341
368
|
const coverage = createSessionEventCoverage();
|
|
369
|
+
const summary = createSessionEventSummary();
|
|
370
|
+
const composition = createSessionEventComposition();
|
|
342
371
|
const readState = createSessionEventReadState({ limit, mode });
|
|
343
372
|
const unmappedTypes = createUnmappedSessionEventTracker();
|
|
344
373
|
const duplicateTexts = createDuplicateTextTracker();
|
|
@@ -350,6 +379,9 @@ export async function readSessionEvents({
|
|
|
350
379
|
});
|
|
351
380
|
|
|
352
381
|
function addEvent(event, pendingId = null) {
|
|
382
|
+
if (event.kind === SESSION_EVENT_KIND.ASK && !event.injected) summary.asks += 1;
|
|
383
|
+
if (event.kind === SESSION_EVENT_KIND.EDIT) summary.edits += 1;
|
|
384
|
+
if (event.kind === SESSION_EVENT_KIND.RAN) summary.commands += 1;
|
|
353
385
|
return readState.add(event, { pendingId });
|
|
354
386
|
}
|
|
355
387
|
|
|
@@ -735,14 +767,17 @@ export async function readSessionEvents({
|
|
|
735
767
|
|
|
736
768
|
if (entry.oversized) {
|
|
737
769
|
coverage.oversized += 1;
|
|
770
|
+
composition.largeRecords += entry.bytes;
|
|
738
771
|
return true;
|
|
739
772
|
}
|
|
740
773
|
|
|
741
774
|
if (!entry.parsed || typeof entry.parsed !== "object") {
|
|
742
775
|
coverage.unparseable += 1;
|
|
776
|
+
composition.other += entry.bytes;
|
|
743
777
|
return true;
|
|
744
778
|
}
|
|
745
779
|
|
|
780
|
+
composition[compositionSegment(entry.parsed)] += entry.bytes;
|
|
746
781
|
const result = handleRecord(entry.parsed, entry.index);
|
|
747
782
|
coverage[result.classification] += 1;
|
|
748
783
|
if (result.duplicate) coverage.duplicates += 1;
|
|
@@ -774,6 +809,8 @@ export async function readSessionEvents({
|
|
|
774
809
|
reason: events.length === 0 && read.complete
|
|
775
810
|
? SESSION_EVENT_REASON.NO_RECOGNIZED_EVENTS
|
|
776
811
|
: null,
|
|
812
|
+
composition: finalizeSessionEventComposition(composition, read.snapshotBytes),
|
|
813
|
+
summary,
|
|
777
814
|
window: readState.window(read),
|
|
778
815
|
});
|
|
779
816
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { constants as fsConstants, createReadStream } from "node:fs";
|
|
3
3
|
import { promises as fs } from "node:fs";
|
|
4
|
-
import os from "node:os";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
import readline from "node:readline";
|
|
7
6
|
|
|
@@ -25,18 +24,7 @@ import {
|
|
|
25
24
|
invalidateCodexDatabaseResolution,
|
|
26
25
|
resolveCodexDatabases,
|
|
27
26
|
} from "./database-families.mjs";
|
|
28
|
-
|
|
29
|
-
function expandHome(value) {
|
|
30
|
-
if (!value || value === "~") {
|
|
31
|
-
return os.homedir();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (value.startsWith("~/")) {
|
|
35
|
-
return path.join(os.homedir(), value.slice(2));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return value;
|
|
39
|
-
}
|
|
27
|
+
import { expandHomePath } from "../../platform.mjs";
|
|
40
28
|
|
|
41
29
|
function normalizeText(value) {
|
|
42
30
|
if (typeof value !== "string") {
|
|
@@ -448,15 +436,17 @@ function deriveDisplayName({
|
|
|
448
436
|
}
|
|
449
437
|
|
|
450
438
|
export function getCodexPaths(codexHomeInput, { refresh = false } = {}) {
|
|
451
|
-
const codexHome = path.resolve(
|
|
439
|
+
const codexHome = path.resolve(expandHomePath(codexHomeInput || "~/.codex"));
|
|
452
440
|
const archivedSessionsDirectory = path.join(codexHome, "archived_sessions");
|
|
453
441
|
const sessionsDirectory = path.join(codexHome, "sessions");
|
|
454
442
|
const resolution = resolveCodexDatabases(codexHome, { refresh });
|
|
455
|
-
const { goals, logs, memories, state } = resolution.families;
|
|
443
|
+
const { goals, logs, memories, queue, state, threadHistory } = resolution.families;
|
|
456
444
|
const stateDatabasePath = state.primary?.path ?? path.join(codexHome, "state_5.sqlite");
|
|
457
445
|
const logsDatabasePath = logs.primary?.path ?? path.join(codexHome, "logs_2.sqlite");
|
|
458
446
|
const memoryDatabasePath = memories.primary?.path ?? path.join(codexHome, "memories_1.sqlite");
|
|
459
447
|
const goalsDatabasePath = goals.primary?.path ?? path.join(codexHome, "goals_1.sqlite");
|
|
448
|
+
const queueDatabasePath = queue.primary?.path ?? path.join(codexHome, "queue_1.sqlite");
|
|
449
|
+
const threadHistoryDatabasePath = threadHistory.primary?.path ?? path.join(codexHome, "thread_history_1.sqlite");
|
|
460
450
|
|
|
461
451
|
return {
|
|
462
452
|
archivedSessionsDirectory,
|
|
@@ -472,6 +462,8 @@ export function getCodexPaths(codexHomeInput, { refresh = false } = {}) {
|
|
|
472
462
|
logsDatabasePaths: allValidDatabases(logs).map((database) => database.path),
|
|
473
463
|
memoryDatabasePath,
|
|
474
464
|
memoryDatabasePaths: allValidDatabases(memories).map((database) => database.path),
|
|
465
|
+
queueDatabasePath,
|
|
466
|
+
queueDatabasePaths: allValidDatabases(queue).map((database) => database.path),
|
|
475
467
|
resolvedDatabases: databaseFamilySummary(resolution),
|
|
476
468
|
sessionIndexPath: path.join(codexHome, "session_index.jsonl"),
|
|
477
469
|
sessionsDirectory,
|
|
@@ -479,6 +471,8 @@ export function getCodexPaths(codexHomeInput, { refresh = false } = {}) {
|
|
|
479
471
|
stateDatabases: allValidDatabases(state),
|
|
480
472
|
stateDatabasePaths: allValidDatabases(state).map((database) => database.path),
|
|
481
473
|
threadWriterLocksDirectory: path.join(codexHome, "thread-writer-locks"),
|
|
474
|
+
threadHistoryDatabasePath,
|
|
475
|
+
threadHistoryDatabasePaths: allValidDatabases(threadHistory).map((database) => database.path),
|
|
482
476
|
transcriptDirectories: [sessionsDirectory, archivedSessionsDirectory],
|
|
483
477
|
};
|
|
484
478
|
}
|
|
@@ -581,12 +575,16 @@ export async function loadSessionStore({ codexHome }) {
|
|
|
581
575
|
hasGoalsDatabase,
|
|
582
576
|
hasLogsDatabase,
|
|
583
577
|
hasMemoryDatabase,
|
|
578
|
+
hasQueueDatabase,
|
|
579
|
+
hasThreadHistoryDatabase,
|
|
584
580
|
] = await Promise.all([
|
|
585
581
|
pathExists(paths.desktopStatePath),
|
|
586
582
|
pathExists(paths.desktopStateBackupPath),
|
|
587
583
|
pathExists(paths.goalsDatabasePath),
|
|
588
584
|
pathExists(paths.logsDatabasePath),
|
|
589
585
|
pathExists(paths.memoryDatabasePath),
|
|
586
|
+
pathExists(paths.queueDatabasePath),
|
|
587
|
+
pathExists(paths.threadHistoryDatabasePath),
|
|
590
588
|
]);
|
|
591
589
|
const threadRowsById = new Map();
|
|
592
590
|
const spawnEdgesByKey = new Map();
|
|
@@ -780,9 +778,12 @@ export async function loadSessionStore({ codexHome }) {
|
|
|
780
778
|
hasGoalsDatabase,
|
|
781
779
|
hasLogsDatabase,
|
|
782
780
|
hasMemoryDatabase,
|
|
781
|
+
hasQueueDatabase,
|
|
782
|
+
hasThreadHistoryDatabase,
|
|
783
783
|
goalsDatabasePath: paths.goalsDatabasePath,
|
|
784
784
|
logsDatabasePath: paths.logsDatabasePath,
|
|
785
785
|
memoryDatabasePath: paths.memoryDatabasePath,
|
|
786
|
+
queueDatabasePath: paths.queueDatabasePath,
|
|
786
787
|
records,
|
|
787
788
|
recordsById: new Map(records.map((record) => [record.id, record])),
|
|
788
789
|
sessionIndexPath: paths.sessionIndexPath,
|
|
@@ -794,6 +795,9 @@ export async function loadSessionStore({ codexHome }) {
|
|
|
794
795
|
resolvedDatabases: paths.resolvedDatabases,
|
|
795
796
|
logsDatabasePaths: paths.logsDatabasePaths,
|
|
796
797
|
memoryDatabasePaths: paths.memoryDatabasePaths,
|
|
798
|
+
queueDatabasePaths: paths.queueDatabasePaths,
|
|
799
|
+
threadHistoryDatabasePath: paths.threadHistoryDatabasePath,
|
|
800
|
+
threadHistoryDatabasePaths: paths.threadHistoryDatabasePaths,
|
|
797
801
|
goalsDatabasePaths: paths.goalsDatabasePaths,
|
|
798
802
|
transcriptHeaders,
|
|
799
803
|
historyPath: paths.historyPath,
|
|
@@ -1017,7 +1021,7 @@ async function getSessionSizeIndex(paths, { refresh = false } = {}) {
|
|
|
1017
1021
|
}
|
|
1018
1022
|
|
|
1019
1023
|
export function invalidateSessionCache({ codexHome }) {
|
|
1020
|
-
const resolvedHome = path.resolve(
|
|
1024
|
+
const resolvedHome = path.resolve(expandHomePath(codexHome || "~/.codex"));
|
|
1021
1025
|
sessionSizeCache.delete(resolvedHome);
|
|
1022
1026
|
invalidateCodexDatabaseResolution(resolvedHome);
|
|
1023
1027
|
}
|
|
@@ -1229,12 +1233,16 @@ async function getStoreAvailability(paths) {
|
|
|
1229
1233
|
hasGoalsDatabase,
|
|
1230
1234
|
hasLogsDatabase,
|
|
1231
1235
|
hasMemoryDatabase,
|
|
1236
|
+
hasQueueDatabase,
|
|
1237
|
+
hasThreadHistoryDatabase,
|
|
1232
1238
|
] = await Promise.all([
|
|
1233
1239
|
pathExists(paths.desktopStatePath),
|
|
1234
1240
|
pathExists(paths.desktopStateBackupPath),
|
|
1235
1241
|
Promise.resolve(paths.goalsDatabasePaths.length > 0),
|
|
1236
1242
|
Promise.resolve(paths.logsDatabasePaths.length > 0),
|
|
1237
1243
|
Promise.resolve(paths.memoryDatabasePaths.length > 0),
|
|
1244
|
+
Promise.resolve(paths.queueDatabasePaths.length > 0),
|
|
1245
|
+
Promise.resolve(paths.threadHistoryDatabasePaths.length > 0),
|
|
1238
1246
|
]);
|
|
1239
1247
|
|
|
1240
1248
|
return {
|
|
@@ -1243,6 +1251,8 @@ async function getStoreAvailability(paths) {
|
|
|
1243
1251
|
hasGoalsDatabase,
|
|
1244
1252
|
hasLogsDatabase,
|
|
1245
1253
|
hasMemoryDatabase,
|
|
1254
|
+
hasQueueDatabase,
|
|
1255
|
+
hasThreadHistoryDatabase,
|
|
1246
1256
|
};
|
|
1247
1257
|
}
|
|
1248
1258
|
|
|
@@ -1497,6 +1507,7 @@ export async function loadDeletionStore({ codexHome, recordIds }) {
|
|
|
1497
1507
|
historyPath: paths.historyPath,
|
|
1498
1508
|
logsDatabasePath: paths.logsDatabasePath,
|
|
1499
1509
|
memoryDatabasePath: paths.memoryDatabasePath,
|
|
1510
|
+
queueDatabasePath: paths.queueDatabasePath,
|
|
1500
1511
|
records,
|
|
1501
1512
|
recordsById: new Map(records.map((record) => [record.id, record])),
|
|
1502
1513
|
sessionIndexPath: paths.sessionIndexPath,
|
|
@@ -1507,6 +1518,9 @@ export async function loadDeletionStore({ codexHome, recordIds }) {
|
|
|
1507
1518
|
resolvedDatabases: paths.resolvedDatabases,
|
|
1508
1519
|
logsDatabasePaths: paths.logsDatabasePaths,
|
|
1509
1520
|
memoryDatabasePaths: paths.memoryDatabasePaths,
|
|
1521
|
+
queueDatabasePaths: paths.queueDatabasePaths,
|
|
1522
|
+
threadHistoryDatabasePath: paths.threadHistoryDatabasePath,
|
|
1523
|
+
threadHistoryDatabasePaths: paths.threadHistoryDatabasePaths,
|
|
1510
1524
|
goalsDatabasePaths: paths.goalsDatabasePaths,
|
|
1511
1525
|
transcriptHeaders: new Map(
|
|
1512
1526
|
[...transcriptHeaders].filter(([id]) => relevantIds.has(id)),
|
|
@@ -1966,6 +1980,19 @@ export async function planSessionDeletion({ recordIds, store }) {
|
|
|
1966
1980
|
.reduce((total, databasePath) => total + countRowsForIds(databasePath, "stage1_outputs", "thread_id", deletionIds), 0);
|
|
1967
1981
|
const goalRowCount = (store.goalsDatabasePaths ?? [store.goalsDatabasePath].filter(Boolean))
|
|
1968
1982
|
.reduce((total, databasePath) => total + countRowsForIds(databasePath, "thread_goals", "thread_id", deletionIds), 0);
|
|
1983
|
+
const queueRowCount = (store.queueDatabasePaths ?? [store.queueDatabasePath].filter(Boolean))
|
|
1984
|
+
.reduce((total, databasePath) => total + countRowsForIds(databasePath, "queued_items", "thread_id", deletionIds), 0);
|
|
1985
|
+
const queueRevisionRowCount = (store.queueDatabasePaths ?? [store.queueDatabasePath].filter(Boolean))
|
|
1986
|
+
.reduce((total, databasePath) => total + (inspectSqliteTable(databasePath, "queued_thread_revisions").exists
|
|
1987
|
+
? countRowsForIds(databasePath, "queued_thread_revisions", "thread_id", deletionIds)
|
|
1988
|
+
: 0), 0);
|
|
1989
|
+
const threadHistoryRowCount = (store.threadHistoryDatabasePaths ?? [store.threadHistoryDatabasePath].filter(Boolean))
|
|
1990
|
+
.reduce((total, databasePath) => total + [
|
|
1991
|
+
"thread_items",
|
|
1992
|
+
"thread_turns",
|
|
1993
|
+
"thread_history_projection_state",
|
|
1994
|
+
].reduce((databaseTotal, tableName) =>
|
|
1995
|
+
databaseTotal + countRowsForIds(databasePath, tableName, "thread_id", deletionIds), 0), 0);
|
|
1969
1996
|
const stateTargets = (store.stateDatabases ?? [{ path: store.stateDatabasePath }]).map((database) => ({
|
|
1970
1997
|
database,
|
|
1971
1998
|
ids: findRowsForIds(database.path, "threads", "id", deletionIds).map((row) => String(row.id)),
|
|
@@ -1984,6 +2011,8 @@ export async function planSessionDeletion({ recordIds, store }) {
|
|
|
1984
2011
|
ids: deletionIds,
|
|
1985
2012
|
logRowCount,
|
|
1986
2013
|
memoryRowCount,
|
|
2014
|
+
queueRowCount,
|
|
2015
|
+
queueRevisionRowCount,
|
|
1987
2016
|
missingTranscriptPaths,
|
|
1988
2017
|
newestLinkedActivityAtMs,
|
|
1989
2018
|
records: selectedRecords,
|
|
@@ -1992,6 +2021,7 @@ export async function planSessionDeletion({ recordIds, store }) {
|
|
|
1992
2021
|
stateTargets,
|
|
1993
2022
|
transcriptBytes,
|
|
1994
2023
|
transcriptFileCount,
|
|
2024
|
+
threadHistoryRowCount,
|
|
1995
2025
|
transcriptPaths,
|
|
1996
2026
|
};
|
|
1997
2027
|
}
|
|
@@ -2003,6 +2033,8 @@ function getBackupSourcePaths({ plan, scope, store }) {
|
|
|
2003
2033
|
const databasePaths = [
|
|
2004
2034
|
...(store.stateDatabasePaths ?? [store.stateDatabasePath]),
|
|
2005
2035
|
...(store.logsDatabasePaths ?? [store.logsDatabasePath].filter(Boolean)),
|
|
2036
|
+
...(store.queueDatabasePaths ?? [store.queueDatabasePath].filter(Boolean)),
|
|
2037
|
+
...(store.threadHistoryDatabasePaths ?? [store.threadHistoryDatabasePath].filter(Boolean)),
|
|
2006
2038
|
...(scope === "deep" ? (store.memoryDatabasePaths ?? [store.memoryDatabasePath].filter(Boolean)) : []),
|
|
2007
2039
|
...(scope === "deep" ? (store.goalsDatabasePaths ?? [store.goalsDatabasePath].filter(Boolean)) : []),
|
|
2008
2040
|
].filter(Boolean);
|
|
@@ -2045,6 +2077,9 @@ export async function fingerprintSessionDeletion({ plan, scope, store }) {
|
|
|
2045
2077
|
plan.dynamicToolRowCount,
|
|
2046
2078
|
plan.historyMatchCount,
|
|
2047
2079
|
plan.logRowCount,
|
|
2080
|
+
plan.queueRowCount,
|
|
2081
|
+
plan.queueRevisionRowCount,
|
|
2082
|
+
plan.threadHistoryRowCount,
|
|
2048
2083
|
plan.sessionIndexMatchCount,
|
|
2049
2084
|
plan.spawnEdgeCount,
|
|
2050
2085
|
...(scope === "deep" ? [plan.goalRowCount, plan.memoryRowCount] : []),
|
|
@@ -2052,6 +2087,8 @@ export async function fingerprintSessionDeletion({ plan, scope, store }) {
|
|
|
2052
2087
|
hash.update(`counts\0${counts.join("\0")}\0`);
|
|
2053
2088
|
hash.update(`stores\0${[
|
|
2054
2089
|
store.hasLogsDatabase,
|
|
2090
|
+
store.hasQueueDatabase,
|
|
2091
|
+
store.hasThreadHistoryDatabase,
|
|
2055
2092
|
...(scope === "deep" ? [
|
|
2056
2093
|
store.hasDesktopState,
|
|
2057
2094
|
store.hasDesktopStateBackup,
|
|
@@ -2091,6 +2128,17 @@ export async function fingerprintSessionDeletion({ plan, scope, store }) {
|
|
|
2091
2128
|
for (const databasePath of store.logsDatabasePaths ?? [store.logsDatabasePath].filter(Boolean)) {
|
|
2092
2129
|
fingerprintRowsForIds(hash, databasePath, "logs", "thread_id", plan.ids);
|
|
2093
2130
|
}
|
|
2131
|
+
for (const databasePath of store.queueDatabasePaths ?? [store.queueDatabasePath].filter(Boolean)) {
|
|
2132
|
+
fingerprintRowsForIds(hash, databasePath, "queued_items", "thread_id", plan.ids);
|
|
2133
|
+
if (inspectSqliteTable(databasePath, "queued_thread_revisions").exists) {
|
|
2134
|
+
fingerprintRowsForIds(hash, databasePath, "queued_thread_revisions", "thread_id", plan.ids);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
for (const databasePath of store.threadHistoryDatabasePaths ?? [store.threadHistoryDatabasePath].filter(Boolean)) {
|
|
2138
|
+
fingerprintRowsForIds(hash, databasePath, "thread_items", "thread_id", plan.ids);
|
|
2139
|
+
fingerprintRowsForIds(hash, databasePath, "thread_turns", "thread_id", plan.ids);
|
|
2140
|
+
fingerprintRowsForIds(hash, databasePath, "thread_history_projection_state", "thread_id", plan.ids);
|
|
2141
|
+
}
|
|
2094
2142
|
if (scope === "deep") {
|
|
2095
2143
|
for (const databasePath of store.memoryDatabasePaths ?? [store.memoryDatabasePath].filter(Boolean)) {
|
|
2096
2144
|
fingerprintRowsForIds(hash, databasePath, "stage1_outputs", "thread_id", plan.ids);
|
|
@@ -2304,6 +2352,8 @@ async function createOperationBackup({ onProgress, plan, scope, store }) {
|
|
|
2304
2352
|
const snapshotCandidates = [
|
|
2305
2353
|
...(store.stateDatabasePaths ?? [store.stateDatabasePath]).map((databasePath) => [databasePath, path.basename(databasePath), true]),
|
|
2306
2354
|
...(store.logsDatabasePaths ?? [store.logsDatabasePath].filter(Boolean)).map((databasePath) => [databasePath, path.basename(databasePath), true]),
|
|
2355
|
+
...(store.queueDatabasePaths ?? [store.queueDatabasePath].filter(Boolean)).map((databasePath) => [databasePath, path.basename(databasePath), true]),
|
|
2356
|
+
...(store.threadHistoryDatabasePaths ?? [store.threadHistoryDatabasePath].filter(Boolean)).map((databasePath) => [databasePath, path.basename(databasePath), true]),
|
|
2307
2357
|
...(scope === "deep" ? (store.memoryDatabasePaths ?? [store.memoryDatabasePath].filter(Boolean)) : []).map((databasePath) => [databasePath, path.basename(databasePath), true]),
|
|
2308
2358
|
...(scope === "deep" ? (store.goalsDatabasePaths ?? [store.goalsDatabasePath].filter(Boolean)) : []).map((databasePath) => [databasePath, path.basename(databasePath), true]),
|
|
2309
2359
|
];
|
|
@@ -2479,6 +2529,25 @@ export async function executeSessionDeletion({
|
|
|
2479
2529
|
executeTransaction(databasePath, deleteStatements("logs", "thread_id", plan.ids));
|
|
2480
2530
|
}
|
|
2481
2531
|
}
|
|
2532
|
+
if (store.hasQueueDatabase) {
|
|
2533
|
+
for (const databasePath of store.queueDatabasePaths ?? [store.queueDatabasePath]) {
|
|
2534
|
+
executeTransaction(databasePath, [
|
|
2535
|
+
...deleteStatements("queued_items", "thread_id", plan.ids),
|
|
2536
|
+
...(inspectSqliteTable(databasePath, "queued_thread_revisions").exists
|
|
2537
|
+
? deleteStatements("queued_thread_revisions", "thread_id", plan.ids)
|
|
2538
|
+
: []),
|
|
2539
|
+
]);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
if (store.hasThreadHistoryDatabase) {
|
|
2543
|
+
for (const databasePath of store.threadHistoryDatabasePaths ?? [store.threadHistoryDatabasePath]) {
|
|
2544
|
+
executeTransaction(databasePath, [
|
|
2545
|
+
...deleteStatements("thread_items", "thread_id", plan.ids),
|
|
2546
|
+
...deleteStatements("thread_turns", "thread_id", plan.ids),
|
|
2547
|
+
...deleteStatements("thread_history_projection_state", "thread_id", plan.ids),
|
|
2548
|
+
]);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2482
2551
|
for (const { database, ids } of plan.stateTargets ?? (store.stateDatabases ?? [{ path: store.stateDatabasePath }]).map((database) => ({ database, ids: plan.ids }))) {
|
|
2483
2552
|
const stateStatements = [];
|
|
2484
2553
|
const schema = stateSchema(database);
|
|
@@ -2840,6 +2909,21 @@ export async function verifySessionDeletion({ plan, scope = "deep", store }) {
|
|
|
2840
2909
|
? (store.logsDatabasePaths ?? [store.logsDatabasePath]).flatMap((databasePath) =>
|
|
2841
2910
|
findRowsForIds(databasePath, "logs", "thread_id", plan.ids, { limitOne: true }))
|
|
2842
2911
|
: [];
|
|
2912
|
+
const remainingQueueRecords = store.hasQueueDatabase
|
|
2913
|
+
? (store.queueDatabasePaths ?? [store.queueDatabasePath]).flatMap((databasePath) => [
|
|
2914
|
+
...findRowsForIds(databasePath, "queued_items", "thread_id", plan.ids, { limitOne: true }),
|
|
2915
|
+
...(inspectSqliteTable(databasePath, "queued_thread_revisions").exists
|
|
2916
|
+
? findRowsForIds(databasePath, "queued_thread_revisions", "thread_id", plan.ids, { limitOne: true })
|
|
2917
|
+
: []),
|
|
2918
|
+
])
|
|
2919
|
+
: [];
|
|
2920
|
+
const remainingThreadHistoryRecords = store.hasThreadHistoryDatabase
|
|
2921
|
+
? (store.threadHistoryDatabasePaths ?? [store.threadHistoryDatabasePath]).flatMap((databasePath) => [
|
|
2922
|
+
...findRowsForIds(databasePath, "thread_items", "thread_id", plan.ids, { limitOne: true }),
|
|
2923
|
+
...findRowsForIds(databasePath, "thread_turns", "thread_id", plan.ids, { limitOne: true }),
|
|
2924
|
+
...findRowsForIds(databasePath, "thread_history_projection_state", "thread_id", plan.ids, { limitOne: true }),
|
|
2925
|
+
])
|
|
2926
|
+
: [];
|
|
2843
2927
|
const [sessionIndexMatches, historyMatches] = await Promise.all([
|
|
2844
2928
|
inspectJsonlMatches(
|
|
2845
2929
|
store.sessionIndexPath,
|
|
@@ -2884,6 +2968,8 @@ export async function verifySessionDeletion({ plan, scope = "deep", store }) {
|
|
|
2884
2968
|
remainingMemoryRecords.length === 0 &&
|
|
2885
2969
|
remainingGoalRecords.length === 0 &&
|
|
2886
2970
|
remainingLogRecords.length === 0 &&
|
|
2971
|
+
remainingQueueRecords.length === 0 &&
|
|
2972
|
+
remainingThreadHistoryRecords.length === 0 &&
|
|
2887
2973
|
sessionIndexMatches.count === 0 &&
|
|
2888
2974
|
historyMatches.count === 0 &&
|
|
2889
2975
|
remainingTranscriptPaths.length === 0 &&
|
|
@@ -2895,6 +2981,8 @@ export async function verifySessionDeletion({ plan, scope = "deep", store }) {
|
|
|
2895
2981
|
remainingHistoryEntryCount: historyMatches.count,
|
|
2896
2982
|
remainingLogRecords,
|
|
2897
2983
|
remainingMemoryRecords,
|
|
2984
|
+
remainingQueueRecords,
|
|
2985
|
+
remainingThreadHistoryRecords,
|
|
2898
2986
|
remainingSessionIndexEntries,
|
|
2899
2987
|
remainingSessionIndexEntryCount: sessionIndexMatches.count,
|
|
2900
2988
|
remainingThreads,
|
package/lib/server.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "./session-event-reader.mjs";
|
|
13
13
|
import { createProviderSettings } from "./settings.mjs";
|
|
14
14
|
import { classifyInstalledVersion } from "./version-support.mjs";
|
|
15
|
+
import { getCommandInvocation } from "./platform.mjs";
|
|
15
16
|
|
|
16
17
|
const MAX_BODY_BYTES = 64 * 1024;
|
|
17
18
|
const ALLOWED_SCOPES = new Set(["core", "deep"]);
|
|
@@ -32,7 +33,12 @@ const staticAssets = new Map([
|
|
|
32
33
|
|
|
33
34
|
function readCommandVersion(command, args) {
|
|
34
35
|
try {
|
|
35
|
-
|
|
36
|
+
const invocation = getCommandInvocation(command, args);
|
|
37
|
+
return execFileSync(invocation.command, invocation.args, {
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
40
|
+
windowsHide: invocation.windowsHide,
|
|
41
|
+
}).trim() || null;
|
|
36
42
|
} catch {
|
|
37
43
|
return null;
|
|
38
44
|
}
|
package/lib/session-events.mjs
CHANGED
|
@@ -206,6 +206,72 @@ export function createSessionEventCoverage({
|
|
|
206
206
|
};
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
export function createSessionEventSummary({ asks = 0, commands = 0, edits = 0 } = {}) {
|
|
210
|
+
return {
|
|
211
|
+
asks: requiredCount(asks, "summary.asks"),
|
|
212
|
+
commands: requiredCount(commands, "summary.commands"),
|
|
213
|
+
edits: requiredCount(edits, "summary.edits"),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Ordered largest-concept-first so the bar and its legend agree
|
|
218
|
+
export const SESSION_EVENT_COMPOSITION_SEGMENTS = Object.freeze([
|
|
219
|
+
"toolOutput",
|
|
220
|
+
"largeRecords",
|
|
221
|
+
"compaction",
|
|
222
|
+
"attachments",
|
|
223
|
+
"messages",
|
|
224
|
+
"edits",
|
|
225
|
+
"reasoning",
|
|
226
|
+
"other",
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
export function createSessionEventComposition({
|
|
230
|
+
attachments = 0,
|
|
231
|
+
compaction = 0,
|
|
232
|
+
edits = 0,
|
|
233
|
+
largeRecords = 0,
|
|
234
|
+
messages = 0,
|
|
235
|
+
other = 0,
|
|
236
|
+
reasoning = 0,
|
|
237
|
+
toolOutput = 0,
|
|
238
|
+
total = 0,
|
|
239
|
+
} = {}) {
|
|
240
|
+
const composition = {
|
|
241
|
+
attachments: requiredCount(attachments, "composition.attachments"),
|
|
242
|
+
compaction: requiredCount(compaction, "composition.compaction"),
|
|
243
|
+
edits: requiredCount(edits, "composition.edits"),
|
|
244
|
+
largeRecords: requiredCount(largeRecords, "composition.largeRecords"),
|
|
245
|
+
messages: requiredCount(messages, "composition.messages"),
|
|
246
|
+
other: requiredCount(other, "composition.other"),
|
|
247
|
+
reasoning: requiredCount(reasoning, "composition.reasoning"),
|
|
248
|
+
toolOutput: requiredCount(toolOutput, "composition.toolOutput"),
|
|
249
|
+
total: requiredCount(total, "composition.total"),
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const segments = SESSION_EVENT_COMPOSITION_SEGMENTS
|
|
253
|
+
.reduce((sum, segment) => sum + composition[segment], 0);
|
|
254
|
+
|
|
255
|
+
if (segments !== composition.total) {
|
|
256
|
+
throw new TypeError("Session event composition segments must add up to the transcript size.");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return composition;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function finalizeSessionEventComposition(composition, transcriptBytes) {
|
|
263
|
+
const attributed = SESSION_EVENT_COMPOSITION_SEGMENTS
|
|
264
|
+
.filter((segment) => segment !== "other")
|
|
265
|
+
.reduce((sum, segment) => sum + (composition[segment] ?? 0), 0);
|
|
266
|
+
const total = Math.max(transcriptBytes ?? 0, attributed);
|
|
267
|
+
|
|
268
|
+
return createSessionEventComposition({
|
|
269
|
+
...composition,
|
|
270
|
+
other: total - attributed,
|
|
271
|
+
total,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
209
275
|
export function sessionEventCoveragePercent(coverage) {
|
|
210
276
|
const normalizedCoverage = createSessionEventCoverage(coverage);
|
|
211
277
|
const considered = normalizedCoverage.total - normalizedCoverage.skipped;
|
|
@@ -246,13 +312,17 @@ export function createSessionEventHeader({
|
|
|
246
312
|
}
|
|
247
313
|
|
|
248
314
|
export function createSessionEventsResult({
|
|
315
|
+
composition,
|
|
249
316
|
coverage,
|
|
250
317
|
events = [],
|
|
251
318
|
header,
|
|
252
319
|
reason = null,
|
|
320
|
+
summary,
|
|
253
321
|
window = {},
|
|
254
322
|
} = {}) {
|
|
255
323
|
const normalizedCoverage = createSessionEventCoverage(coverage);
|
|
324
|
+
const normalizedSummary = createSessionEventSummary(summary);
|
|
325
|
+
const normalizedComposition = createSessionEventComposition(composition);
|
|
256
326
|
|
|
257
327
|
if (
|
|
258
328
|
normalizedCoverage.recognized
|
|
@@ -298,6 +368,8 @@ export function createSessionEventsResult({
|
|
|
298
368
|
events: [...events],
|
|
299
369
|
header: createSessionEventHeader(header),
|
|
300
370
|
reason,
|
|
371
|
+
composition: normalizedComposition,
|
|
372
|
+
summary: normalizedSummary,
|
|
301
373
|
window: {
|
|
302
374
|
complete,
|
|
303
375
|
end,
|
package/lib/settings.mjs
CHANGED
|
@@ -3,6 +3,11 @@ import { promises as fs } from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
|
+
import {
|
|
7
|
+
expandHomePath,
|
|
8
|
+
getDefaultConfigDirectory as resolveDefaultConfigDirectory,
|
|
9
|
+
} from "./platform.mjs";
|
|
10
|
+
|
|
6
11
|
const CONFIG_VERSION = 1;
|
|
7
12
|
const PROVIDERS = {
|
|
8
13
|
codex: {
|
|
@@ -21,24 +26,12 @@ const PROVIDERS = {
|
|
|
21
26
|
},
|
|
22
27
|
};
|
|
23
28
|
|
|
24
|
-
function expandHome(value) {
|
|
25
|
-
if (value === "~") {
|
|
26
|
-
return os.homedir();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (value.startsWith("~/")) {
|
|
30
|
-
return path.join(os.homedir(), value.slice(2));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return value;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
29
|
function normalizeHome(value) {
|
|
37
30
|
if (typeof value !== "string" || value.includes("\0")) {
|
|
38
31
|
throw new Error("Enter a valid folder path.");
|
|
39
32
|
}
|
|
40
33
|
|
|
41
|
-
const expanded =
|
|
34
|
+
const expanded = expandHomePath(value.trim());
|
|
42
35
|
|
|
43
36
|
if (!expanded || !path.isAbsolute(expanded)) {
|
|
44
37
|
throw new Error("Enter a full folder path, such as ~/.codex.");
|
|
@@ -58,17 +51,7 @@ function getProviderDefinition(providerId) {
|
|
|
58
51
|
}
|
|
59
52
|
|
|
60
53
|
export function getDefaultConfigDirectory() {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (xdgConfigHome && path.isAbsolute(xdgConfigHome)) {
|
|
64
|
-
return path.join(xdgConfigHome, "session-steward");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (process.platform === "darwin") {
|
|
68
|
-
return path.join(os.homedir(), "Library", "Application Support", "session-steward");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return path.join(os.homedir(), ".config", "session-steward");
|
|
54
|
+
return resolveDefaultConfigDirectory();
|
|
72
55
|
}
|
|
73
56
|
|
|
74
57
|
async function readConfig(configPath) {
|