session-steward 0.2.0 → 0.4.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.
@@ -0,0 +1,177 @@
1
+ import { readdirSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { queryRows } from "../../storage/sqlite.mjs";
5
+
6
+ const CACHE_TTL_MS = 2_000;
7
+
8
+ export const CODEX_DATABASE_PROFILE = Object.freeze({
9
+ id: "codex-local-store-2026-08",
10
+ builtFor: {
11
+ chatgptDesktop: ["26.727.40816"],
12
+ codexCli: ["0.144.1", "0.146.0"],
13
+ },
14
+ });
15
+
16
+ const SCHEMA_REQUIREMENTS = Object.freeze({
17
+ state: {
18
+ fallback: "state_5.sqlite",
19
+ pattern: /^state_(\d+)\.sqlite$/u,
20
+ required: true,
21
+ tables: [{ name: "threads", requiredColumns: ["id", "rollout_path"] }],
22
+ },
23
+ logs: {
24
+ fallback: "logs_2.sqlite",
25
+ pattern: /^logs_(\d+)\.sqlite$/u,
26
+ required: false,
27
+ tables: [{ name: "logs", requiredColumns: ["thread_id"] }],
28
+ },
29
+ memories: {
30
+ fallback: "memories_1.sqlite",
31
+ pattern: /^memories_(\d+)\.sqlite$/u,
32
+ required: false,
33
+ tables: [{ name: "stage1_outputs", requiredColumns: ["thread_id"] }],
34
+ },
35
+ goals: {
36
+ fallback: "goals_1.sqlite",
37
+ pattern: /^goals_(\d+)\.sqlite$/u,
38
+ required: false,
39
+ tables: [
40
+ { name: "thread_goals", requiredColumns: ["thread_id"] },
41
+ { name: "thread_goal_continuation_deferrals", requiredColumns: ["thread_id"] },
42
+ ],
43
+ },
44
+ });
45
+
46
+ const resolutionCache = new Map();
47
+
48
+ function inspectTable(databasePath, tableName) {
49
+ const exists = queryRows(
50
+ databasePath,
51
+ "select name from sqlite_master where type = 'table' and name = ?",
52
+ [tableName],
53
+ ).length > 0;
54
+ if (!exists) return { columns: new Set(), exists: false };
55
+ return {
56
+ columns: new Set(
57
+ queryRows(databasePath, "select name from pragma_table_info(?)", [tableName])
58
+ .map((column) => String(column.name)),
59
+ ),
60
+ exists: true,
61
+ };
62
+ }
63
+
64
+ function inspectCandidate(codexHome, filename, version, family) {
65
+ const databasePath = path.join(codexHome, filename);
66
+ try {
67
+ const tables = Object.fromEntries(
68
+ family.tables.map((requirement) => {
69
+ const inspection = inspectTable(databasePath, requirement.name);
70
+ const missingColumns = requirement.requiredColumns.filter(
71
+ (column) => !inspection.columns.has(column),
72
+ );
73
+ return [requirement.name, { ...inspection, missingColumns }];
74
+ }),
75
+ );
76
+ const invalidTable = family.tables.find((requirement) => {
77
+ const table = tables[requirement.name];
78
+ return !table.exists || table.missingColumns.length > 0;
79
+ });
80
+ return {
81
+ filename,
82
+ path: databasePath,
83
+ reason: invalidTable
84
+ ? !tables[invalidTable.name].exists
85
+ ? `Missing table: ${invalidTable.name}`
86
+ : `Missing fields in ${invalidTable.name}: ${tables[invalidTable.name].missingColumns.join(", ")}`
87
+ : null,
88
+ tables,
89
+ valid: !invalidTable,
90
+ version,
91
+ };
92
+ } catch (error) {
93
+ return {
94
+ filename,
95
+ path: databasePath,
96
+ reason: error instanceof Error ? error.message : "Database could not be inspected.",
97
+ tables: {},
98
+ valid: false,
99
+ version,
100
+ };
101
+ }
102
+ }
103
+
104
+ function fallbackResolution(codexHome) {
105
+ return Object.fromEntries(Object.entries(SCHEMA_REQUIREMENTS).map(([name, family]) => [name, {
106
+ invalid: [],
107
+ primary: {
108
+ filename: family.fallback,
109
+ path: path.join(codexHome, family.fallback),
110
+ tables: {},
111
+ valid: true,
112
+ version: Number(family.pattern.exec(family.fallback)?.[1] ?? 0),
113
+ },
114
+ required: family.required,
115
+ secondaries: [],
116
+ }]));
117
+ }
118
+
119
+ function discover(codexHome) {
120
+ let names;
121
+ try {
122
+ names = readdirSync(codexHome, { withFileTypes: true })
123
+ .filter((entry) => entry.isFile())
124
+ .map((entry) => entry.name);
125
+ } catch {
126
+ return { families: fallbackResolution(codexHome), readable: false };
127
+ }
128
+
129
+ const families = {};
130
+ for (const [name, family] of Object.entries(SCHEMA_REQUIREMENTS)) {
131
+ const candidates = names.flatMap((filename) => {
132
+ const match = family.pattern.exec(filename);
133
+ return match ? [{ filename, version: Number(match[1]) }] : [];
134
+ }).sort((left, right) => right.version - left.version || left.filename.localeCompare(right.filename));
135
+ const inspected = candidates.map((candidate) => inspectCandidate(
136
+ codexHome,
137
+ candidate.filename,
138
+ candidate.version,
139
+ family,
140
+ ));
141
+ const valid = inspected.filter((candidate) => candidate.valid);
142
+ families[name] = {
143
+ invalid: inspected.filter((candidate) => !candidate.valid),
144
+ primary: valid[0] ?? null,
145
+ required: family.required,
146
+ secondaries: valid.slice(1),
147
+ };
148
+ }
149
+ return { families, readable: true };
150
+ }
151
+
152
+ export function resolveCodexDatabases(codexHomeInput, { refresh = false } = {}) {
153
+ const codexHome = path.resolve(codexHomeInput);
154
+ const cached = resolutionCache.get(codexHome);
155
+ if (!refresh && cached?.expiresAtMs > Date.now()) return cached.value;
156
+ const value = discover(codexHome);
157
+ resolutionCache.set(codexHome, { expiresAtMs: Date.now() + CACHE_TTL_MS, value });
158
+ return value;
159
+ }
160
+
161
+ export function invalidateCodexDatabaseResolution(codexHomeInput) {
162
+ resolutionCache.delete(path.resolve(codexHomeInput));
163
+ }
164
+
165
+ export function databaseFamilySummary(resolution) {
166
+ return Object.fromEntries(Object.entries(resolution.families).map(([name, family]) => [name, {
167
+ invalid: family.invalid.map(({ filename, reason, version }) => ({ filename, reason, version })),
168
+ primary: family.primary
169
+ ? { filename: family.primary.filename, version: family.primary.version }
170
+ : null,
171
+ secondaries: family.secondaries.map(({ filename, version }) => ({ filename, version })),
172
+ }]));
173
+ }
174
+
175
+ export function allValidDatabases(family) {
176
+ return family.primary ? [family.primary, ...family.secondaries] : [];
177
+ }
@@ -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,