session-steward 0.3.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.
- package/CHANGELOG.md +16 -0
- package/README.md +14 -14
- package/dist/assets/index-BQ6SUqXr.css +2 -0
- package/dist/assets/{index-fUX3qen0.js → index-QhQbSn0H.js} +1 -1
- package/dist/index.html +2 -2
- package/lib/cli.mjs +5 -0
- package/lib/providers/claude-code/store.mjs +60 -15
- package/lib/providers/codex/database-families.mjs +177 -0
- package/lib/providers/codex/store.mjs +466 -329
- package/lib/server.mjs +8 -3
- package/package.json +2 -1
- package/dist/assets/index-CN9iax_v.css +0 -2
|
@@ -18,6 +18,13 @@ import {
|
|
|
18
18
|
placeholders,
|
|
19
19
|
queryRows,
|
|
20
20
|
} from "../../storage/sqlite.mjs";
|
|
21
|
+
import {
|
|
22
|
+
allValidDatabases,
|
|
23
|
+
CODEX_DATABASE_PROFILE,
|
|
24
|
+
databaseFamilySummary,
|
|
25
|
+
invalidateCodexDatabaseResolution,
|
|
26
|
+
resolveCodexDatabases,
|
|
27
|
+
} from "./database-families.mjs";
|
|
21
28
|
|
|
22
29
|
function expandHome(value) {
|
|
23
30
|
if (!value || value === "~") {
|
|
@@ -440,23 +447,36 @@ function deriveDisplayName({
|
|
|
440
447
|
};
|
|
441
448
|
}
|
|
442
449
|
|
|
443
|
-
function getCodexPaths(codexHomeInput) {
|
|
450
|
+
export function getCodexPaths(codexHomeInput, { refresh = false } = {}) {
|
|
444
451
|
const codexHome = path.resolve(expandHome(codexHomeInput || "~/.codex"));
|
|
445
452
|
const archivedSessionsDirectory = path.join(codexHome, "archived_sessions");
|
|
446
453
|
const sessionsDirectory = path.join(codexHome, "sessions");
|
|
454
|
+
const resolution = resolveCodexDatabases(codexHome, { refresh });
|
|
455
|
+
const { goals, logs, memories, state } = resolution.families;
|
|
456
|
+
const stateDatabasePath = state.primary?.path ?? path.join(codexHome, "state_5.sqlite");
|
|
457
|
+
const logsDatabasePath = logs.primary?.path ?? path.join(codexHome, "logs_2.sqlite");
|
|
458
|
+
const memoryDatabasePath = memories.primary?.path ?? path.join(codexHome, "memories_1.sqlite");
|
|
459
|
+
const goalsDatabasePath = goals.primary?.path ?? path.join(codexHome, "goals_1.sqlite");
|
|
447
460
|
|
|
448
461
|
return {
|
|
449
462
|
archivedSessionsDirectory,
|
|
450
463
|
codexHome,
|
|
451
464
|
desktopStateBackupPath: path.join(codexHome, ".codex-global-state.json.bak"),
|
|
452
465
|
desktopStatePath: path.join(codexHome, ".codex-global-state.json"),
|
|
453
|
-
|
|
466
|
+
databaseFamilies: resolution.families,
|
|
467
|
+
goalsDatabasePath,
|
|
468
|
+
goalsDatabasePaths: allValidDatabases(goals).map((database) => database.path),
|
|
454
469
|
historyPath: path.join(codexHome, "history.jsonl"),
|
|
455
|
-
logsDatabasePath
|
|
456
|
-
|
|
470
|
+
logsDatabasePath,
|
|
471
|
+
logsDatabasePaths: allValidDatabases(logs).map((database) => database.path),
|
|
472
|
+
memoryDatabasePath,
|
|
473
|
+
memoryDatabasePaths: allValidDatabases(memories).map((database) => database.path),
|
|
474
|
+
resolvedDatabases: databaseFamilySummary(resolution),
|
|
457
475
|
sessionIndexPath: path.join(codexHome, "session_index.jsonl"),
|
|
458
476
|
sessionsDirectory,
|
|
459
|
-
stateDatabasePath
|
|
477
|
+
stateDatabasePath,
|
|
478
|
+
stateDatabases: allValidDatabases(state),
|
|
479
|
+
stateDatabasePaths: allValidDatabases(state).map((database) => database.path),
|
|
460
480
|
transcriptDirectories: [sessionsDirectory, archivedSessionsDirectory],
|
|
461
481
|
};
|
|
462
482
|
}
|
|
@@ -470,27 +490,6 @@ const DESKTOP_THREAD_MAP_KEYS = [
|
|
|
470
490
|
|
|
471
491
|
const DESKTOP_THREAD_ARRAY_KEYS = ["projectless-thread-ids"];
|
|
472
492
|
|
|
473
|
-
const COMPATIBILITY_PROFILE = {
|
|
474
|
-
id: "local-store-2026-07",
|
|
475
|
-
builtFor: {
|
|
476
|
-
chatgptDesktop: ["26.727.40816"],
|
|
477
|
-
codexCli: ["0.144.1", "0.146.0"],
|
|
478
|
-
},
|
|
479
|
-
};
|
|
480
|
-
|
|
481
|
-
const SCHEMA_REQUIREMENTS = [
|
|
482
|
-
{
|
|
483
|
-
database: "state_5.sqlite",
|
|
484
|
-
required: true,
|
|
485
|
-
tables: [
|
|
486
|
-
{ name: "threads", columns: ["id", "rollout_path", "cwd", "title", "first_user_message", "agent_nickname", "agent_role", "archived", "is_pinned"] },
|
|
487
|
-
{ name: "thread_spawn_edges", columns: ["parent_thread_id", "child_thread_id", "status"] },
|
|
488
|
-
],
|
|
489
|
-
},
|
|
490
|
-
{ database: "logs_2.sqlite", required: false, tables: [{ name: "logs", columns: ["thread_id"] }] },
|
|
491
|
-
{ database: "memories_1.sqlite", required: false, tables: [{ name: "stage1_outputs", columns: ["thread_id"] }] },
|
|
492
|
-
{ database: "goals_1.sqlite", required: false, tables: [{ name: "thread_goals", columns: ["thread_id"] }, { name: "thread_goal_continuation_deferrals", columns: ["thread_id"] }] },
|
|
493
|
-
];
|
|
494
493
|
|
|
495
494
|
function getMatchingDesktopStateEntryCount(state, deletedIdSet) {
|
|
496
495
|
if (!state || typeof state !== "object" || Array.isArray(state)) {
|
|
@@ -587,35 +586,22 @@ export async function loadSessionStore({ codexHome }) {
|
|
|
587
586
|
pathExists(paths.logsDatabasePath),
|
|
588
587
|
pathExists(paths.memoryDatabasePath),
|
|
589
588
|
]);
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
order by updated_at_ms desc, updated_at desc
|
|
607
|
-
`,
|
|
608
|
-
);
|
|
609
|
-
const spawnEdges = queryRows(
|
|
610
|
-
paths.stateDatabasePath,
|
|
611
|
-
`
|
|
612
|
-
select
|
|
613
|
-
parent_thread_id,
|
|
614
|
-
child_thread_id,
|
|
615
|
-
status
|
|
616
|
-
from thread_spawn_edges
|
|
617
|
-
`,
|
|
618
|
-
);
|
|
589
|
+
const threadRowsById = new Map();
|
|
590
|
+
const spawnEdgesByKey = new Map();
|
|
591
|
+
for (const database of paths.stateDatabases) {
|
|
592
|
+
for (const row of queryRows(database.path, `select ${sessionColumns(database)} from threads t`)) {
|
|
593
|
+
const id = String(row.id);
|
|
594
|
+
if (!threadRowsById.has(id)) threadRowsById.set(id, { ...row, _stateDatabasePath: database.path });
|
|
595
|
+
}
|
|
596
|
+
if (stateSchema(database).hasSpawnEdges) {
|
|
597
|
+
for (const edge of queryRows(database.path, "select parent_thread_id, child_thread_id, status from thread_spawn_edges")) {
|
|
598
|
+
const key = `${edge.parent_thread_id}\0${edge.child_thread_id}`;
|
|
599
|
+
if (!spawnEdgesByKey.has(key)) spawnEdgesByKey.set(key, edge);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const threadRows = [...threadRowsById.values()];
|
|
604
|
+
const spawnEdges = [...spawnEdgesByKey.values()];
|
|
619
605
|
const transcriptHeaders = await indexTranscriptHeaders(paths.transcriptDirectories);
|
|
620
606
|
|
|
621
607
|
const discoveryIds = new Set(threadRows.map((threadRow) => String(threadRow.id)));
|
|
@@ -673,7 +659,7 @@ export async function loadSessionStore({ codexHome }) {
|
|
|
673
659
|
agentRole: threadRow.agent_role ?? transcriptHeader?.agentRole ?? null,
|
|
674
660
|
archived: Boolean(threadRow.archived),
|
|
675
661
|
childThreadIds: childIdsByParentId.get(threadRow.id) ?? [],
|
|
676
|
-
createdAtMs: toTimestampMs(threadRow.created_at_ms),
|
|
662
|
+
createdAtMs: toTimestampMs(threadRow.created_at_ms_resolved ?? threadRow.created_at_ms),
|
|
677
663
|
cwd: threadRow.cwd ?? transcriptHeader?.cwd ?? "",
|
|
678
664
|
displayName: "",
|
|
679
665
|
firstUserMessage: threadRow.first_user_message ?? "",
|
|
@@ -691,7 +677,8 @@ export async function loadSessionStore({ codexHome }) {
|
|
|
691
677
|
rolloutPath,
|
|
692
678
|
title: threadRow.title ?? "",
|
|
693
679
|
titleSource: "",
|
|
694
|
-
updatedAtMs: toTimestampMs(threadRow.updated_at_ms),
|
|
680
|
+
updatedAtMs: toTimestampMs(threadRow.updated_at_ms_resolved ?? threadRow.updated_at_ms),
|
|
681
|
+
_stateDatabasePath: threadRow._stateDatabasePath,
|
|
695
682
|
};
|
|
696
683
|
|
|
697
684
|
if (
|
|
@@ -798,6 +785,12 @@ export async function loadSessionStore({ codexHome }) {
|
|
|
798
785
|
sessionIndexPath: paths.sessionIndexPath,
|
|
799
786
|
spawnEdges,
|
|
800
787
|
stateDatabasePath: paths.stateDatabasePath,
|
|
788
|
+
stateDatabasePaths: paths.stateDatabasePaths,
|
|
789
|
+
stateDatabases: paths.stateDatabases,
|
|
790
|
+
resolvedDatabases: paths.resolvedDatabases,
|
|
791
|
+
logsDatabasePaths: paths.logsDatabasePaths,
|
|
792
|
+
memoryDatabasePaths: paths.memoryDatabasePaths,
|
|
793
|
+
goalsDatabasePaths: paths.goalsDatabasePaths,
|
|
801
794
|
transcriptHeaders,
|
|
802
795
|
historyPath: paths.historyPath,
|
|
803
796
|
};
|
|
@@ -824,51 +817,21 @@ function inspectSqliteTable(databasePath, tableName) {
|
|
|
824
817
|
}
|
|
825
818
|
|
|
826
819
|
export async function diagnoseStorageCompatibility({ codexHome }) {
|
|
827
|
-
const paths = getCodexPaths(codexHome);
|
|
828
|
-
const recognizedDatabases = new Set(SCHEMA_REQUIREMENTS.map((requirement) => requirement.database));
|
|
820
|
+
const paths = getCodexPaths(codexHome, { refresh: true });
|
|
829
821
|
const missing = [];
|
|
830
822
|
const changed = [];
|
|
831
823
|
const available = [];
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
if (requirement.required) {
|
|
838
|
-
missing.push(`Required session database is missing: ${requirement.database}`);
|
|
839
|
-
} else {
|
|
840
|
-
available.push(`Not present: ${requirement.database}`);
|
|
841
|
-
}
|
|
842
|
-
continue;
|
|
824
|
+
for (const [familyName, family] of Object.entries(paths.databaseFamilies)) {
|
|
825
|
+
if (!family.primary && family.required) {
|
|
826
|
+
missing.push(`A supported ${familyName} session database was not found.`);
|
|
827
|
+
} else if (!family.primary) {
|
|
828
|
+
available.push(`Not present: ${familyName} database`);
|
|
843
829
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
for (const table of requirement.tables) {
|
|
848
|
-
const inspection = inspectSqliteTable(databasePath, table.name);
|
|
849
|
-
|
|
850
|
-
if (inspection.error) {
|
|
851
|
-
changed.push(`Could not read ${requirement.database}.`);
|
|
852
|
-
databaseChanged = true;
|
|
853
|
-
continue;
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
if (!inspection.exists) {
|
|
857
|
-
changed.push(`Expected table is missing in ${requirement.database}: ${table.name}.`);
|
|
858
|
-
databaseChanged = true;
|
|
859
|
-
continue;
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
const missingColumns = table.columns.filter((column) => !inspection.columns.includes(column));
|
|
863
|
-
|
|
864
|
-
if (missingColumns.length > 0) {
|
|
865
|
-
changed.push(`Expected fields changed in ${requirement.database}: ${table.name}.`);
|
|
866
|
-
databaseChanged = true;
|
|
867
|
-
}
|
|
830
|
+
for (const database of allValidDatabases(family)) {
|
|
831
|
+
available.push(`Supported: ${database.filename}`);
|
|
868
832
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
available.push(`Supported: ${requirement.database}`);
|
|
833
|
+
for (const database of family.invalid) {
|
|
834
|
+
changed.push(`Could not use ${database.filename}: ${database.reason}`);
|
|
872
835
|
}
|
|
873
836
|
}
|
|
874
837
|
|
|
@@ -879,23 +842,28 @@ export async function diagnoseStorageCompatibility({ codexHome }) {
|
|
|
879
842
|
missing.push("The local Codex folder could not be read.");
|
|
880
843
|
}
|
|
881
844
|
|
|
845
|
+
const recognizedDatabases = new Set(Object.values(paths.databaseFamilies).flatMap((family) => [
|
|
846
|
+
...allValidDatabases(family),
|
|
847
|
+
...family.invalid,
|
|
848
|
+
]).map((database) => database.filename));
|
|
882
849
|
const newlyDiscovered = entries
|
|
883
850
|
.filter((entry) => entry.isFile() && entry.name.endsWith(".sqlite") && !recognizedDatabases.has(entry.name))
|
|
884
851
|
.map((entry) => `Other local database found: ${entry.name}`)
|
|
885
852
|
.sort();
|
|
886
853
|
const status = missing.length > 0 || changed.length > 0
|
|
887
|
-
? "
|
|
854
|
+
? "unsupported"
|
|
888
855
|
: newlyDiscovered.length > 0
|
|
889
|
-
? "
|
|
856
|
+
? "partial"
|
|
890
857
|
: "ready";
|
|
891
858
|
|
|
892
859
|
return {
|
|
893
860
|
available,
|
|
894
|
-
builtFor:
|
|
861
|
+
builtFor: CODEX_DATABASE_PROFILE.builtFor,
|
|
895
862
|
changed,
|
|
896
863
|
missing,
|
|
897
864
|
newlyDiscovered,
|
|
898
|
-
profileId:
|
|
865
|
+
profileId: CODEX_DATABASE_PROFILE.id,
|
|
866
|
+
resolvedDatabases: paths.resolvedDatabases,
|
|
899
867
|
status,
|
|
900
868
|
};
|
|
901
869
|
}
|
|
@@ -903,11 +871,7 @@ export async function diagnoseStorageCompatibility({ codexHome }) {
|
|
|
903
871
|
export async function assertDeepCleanupSupported({ codexHome }) {
|
|
904
872
|
const diagnostic = await diagnoseStorageCompatibility({ codexHome });
|
|
905
873
|
|
|
906
|
-
if (diagnostic.status === "
|
|
907
|
-
throw new Error("Deep cleanup is paused because unrecognized Codex storage was found.");
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
if (diagnostic.status !== "ready") {
|
|
874
|
+
if (diagnostic.status === "unsupported") {
|
|
911
875
|
throw new Error("Deep cleanup is paused because this Codex storage layout is not supported.");
|
|
912
876
|
}
|
|
913
877
|
|
|
@@ -925,12 +889,60 @@ const SESSION_UPDATED_SQL = "coalesce(nullif(t.updated_at_ms, 0), nullif(t.updat
|
|
|
925
889
|
const SESSION_ACTIVITY_SQL = `coalesce(nullif(${SESSION_UPDATED_SQL}, 0), nullif(${SESSION_CREATED_SQL}, 0), 0)`;
|
|
926
890
|
const SESSION_SORTS = new Set(["created", "cwd", "name", "size", "updated"]);
|
|
927
891
|
const sessionSizeCache = new Map();
|
|
892
|
+
const stateSchemaCache = new WeakMap();
|
|
893
|
+
|
|
894
|
+
const OPTIONAL_THREAD_COLUMNS = [
|
|
895
|
+
"agent_nickname", "agent_role", "archived", "cwd", "first_user_message",
|
|
896
|
+
"is_pinned", "title",
|
|
897
|
+
];
|
|
898
|
+
|
|
899
|
+
function stateSchema(database) {
|
|
900
|
+
const known = stateSchemaCache.get(database);
|
|
901
|
+
if (known) return known;
|
|
902
|
+
const cached = database.tables?.threads;
|
|
903
|
+
const columns = cached?.columns?.size
|
|
904
|
+
? cached.columns
|
|
905
|
+
: new Set(inspectSqliteTable(database.path, "threads").columns ?? []);
|
|
906
|
+
const spawn = database.tables?.thread_spawn_edges?.exists !== undefined
|
|
907
|
+
? database.tables.thread_spawn_edges
|
|
908
|
+
: inspectSqliteTable(database.path, "thread_spawn_edges");
|
|
909
|
+
const schema = { columns, hasSpawnEdges: Boolean(spawn?.exists) };
|
|
910
|
+
stateSchemaCache.set(database, schema);
|
|
911
|
+
return schema;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function columnExpression(columns, column, fallback = "null") {
|
|
915
|
+
return columns.has(column) ? `t.${column}` : fallback;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function timestampExpression(columns, prefix) {
|
|
919
|
+
const expressions = [];
|
|
920
|
+
if (columns.has(`${prefix}_at_ms`)) expressions.push(`nullif(t.${prefix}_at_ms, 0)`);
|
|
921
|
+
if (columns.has(`${prefix}_at`)) expressions.push(`nullif(t.${prefix}_at, 0) * 1000`);
|
|
922
|
+
return expressions.length ? `coalesce(${expressions.join(", ")}, 0)` : "0";
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function stateExpressions(database) {
|
|
926
|
+
const schema = stateSchema(database);
|
|
927
|
+
const created = timestampExpression(schema.columns, "created");
|
|
928
|
+
const updated = timestampExpression(schema.columns, "updated");
|
|
929
|
+
return { ...schema, activity: `coalesce(nullif(${updated}, 0), nullif(${created}, 0), 0)`, created, updated };
|
|
930
|
+
}
|
|
928
931
|
|
|
929
932
|
async function buildSessionSizeIndex(paths) {
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
"select id, rollout_path from threads"
|
|
933
|
-
|
|
933
|
+
let rows;
|
|
934
|
+
if (paths.stateDatabases.length === 1) {
|
|
935
|
+
rows = queryRows(paths.stateDatabases[0].path, "select id, rollout_path from threads");
|
|
936
|
+
} else {
|
|
937
|
+
const rowsById = new Map();
|
|
938
|
+
for (const database of paths.stateDatabases) {
|
|
939
|
+
for (const row of queryRows(database.path, "select id, rollout_path from threads")) {
|
|
940
|
+
const id = String(row.id);
|
|
941
|
+
if (!rowsById.has(id)) rowsById.set(id, row);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
rows = [...rowsById.values()];
|
|
945
|
+
}
|
|
934
946
|
const sizes = new Map();
|
|
935
947
|
let nextIndex = 0;
|
|
936
948
|
|
|
@@ -984,7 +996,9 @@ async function getSessionSizeIndex(paths, { refresh = false } = {}) {
|
|
|
984
996
|
}
|
|
985
997
|
|
|
986
998
|
export function invalidateSessionCache({ codexHome }) {
|
|
987
|
-
|
|
999
|
+
const resolvedHome = path.resolve(expandHome(codexHome || "~/.codex"));
|
|
1000
|
+
sessionSizeCache.delete(resolvedHome);
|
|
1001
|
+
invalidateCodexDatabaseResolution(resolvedHome);
|
|
988
1002
|
}
|
|
989
1003
|
|
|
990
1004
|
function compareSessionIdsBySize(leftId, rightId, sizes) {
|
|
@@ -1000,18 +1014,48 @@ function compareSessionIdsBySize(leftId, rightId, sizes) {
|
|
|
1000
1014
|
return (rightSize ?? 0) - (leftSize ?? 0) || leftId.localeCompare(rightId);
|
|
1001
1015
|
}
|
|
1002
1016
|
|
|
1003
|
-
function
|
|
1004
|
-
const
|
|
1017
|
+
function getSessionSort(sort, database) {
|
|
1018
|
+
const { activity, columns, created } = stateExpressions(database);
|
|
1019
|
+
const nameParts = ["t.title", "t.first_user_message"].filter((column) => columns.has(column.slice(2)))
|
|
1020
|
+
.map((column) => `nullif(trim(${column}), '')`);
|
|
1021
|
+
const name = `lower(coalesce(${[...nameParts, "t.id"].join(", ")}))`;
|
|
1022
|
+
const cwd = columns.has("cwd") ? "lower(coalesce(t.cwd, ''))" : "''";
|
|
1005
1023
|
|
|
1006
1024
|
return {
|
|
1007
|
-
created:
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1025
|
+
created: {
|
|
1026
|
+
compare: (left, right) => Number(right.sort_number) - Number(left.sort_number),
|
|
1027
|
+
order: `${created} desc, t.id asc`,
|
|
1028
|
+
selection: `${created} as sort_number`,
|
|
1029
|
+
},
|
|
1030
|
+
cwd: {
|
|
1031
|
+
compare: (left, right) => String(left.sort_text).localeCompare(String(right.sort_text))
|
|
1032
|
+
|| Number(right.sort_activity) - Number(left.sort_activity),
|
|
1033
|
+
order: `${cwd} asc, ${activity} desc, t.id asc`,
|
|
1034
|
+
selection: `${cwd} as sort_text, ${activity} as sort_activity`,
|
|
1035
|
+
},
|
|
1036
|
+
name: {
|
|
1037
|
+
compare: (left, right) => String(left.sort_text).localeCompare(String(right.sort_text))
|
|
1038
|
+
|| Number(right.sort_activity) - Number(left.sort_activity),
|
|
1039
|
+
order: `${name} asc, ${activity} desc, t.id asc`,
|
|
1040
|
+
selection: `${name} as sort_text, ${activity} as sort_activity`,
|
|
1041
|
+
},
|
|
1042
|
+
updated: {
|
|
1043
|
+
compare: (left, right) => Number(right.sort_number) - Number(left.sort_number),
|
|
1044
|
+
order: `${activity} desc, t.id asc`,
|
|
1045
|
+
selection: `${activity} as sort_number`,
|
|
1046
|
+
},
|
|
1047
|
+
}[sort] ?? {
|
|
1048
|
+
compare: (left, right) => Number(right.sort_number) - Number(left.sort_number),
|
|
1049
|
+
order: `${activity} desc, t.id asc`,
|
|
1050
|
+
selection: `${activity} as sort_number`,
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function getSessionOrder(sort, database) {
|
|
1055
|
+
return getSessionSort(sort, database).order;
|
|
1012
1056
|
}
|
|
1013
1057
|
|
|
1014
|
-
function getSessionConditions({
|
|
1058
|
+
function getSessionConditions(database, {
|
|
1015
1059
|
archiveStatus,
|
|
1016
1060
|
inactiveBeforeMs,
|
|
1017
1061
|
includeInternals,
|
|
@@ -1019,32 +1063,34 @@ function getSessionConditions({
|
|
|
1019
1063
|
search,
|
|
1020
1064
|
workspace,
|
|
1021
1065
|
}) {
|
|
1066
|
+
const { activity, columns, hasSpawnEdges } = stateExpressions(database);
|
|
1022
1067
|
const conditions = [];
|
|
1023
1068
|
const parameters = [];
|
|
1024
1069
|
|
|
1025
|
-
if (archiveStatus === "active") {
|
|
1070
|
+
if (archiveStatus === "active" && columns.has("archived")) {
|
|
1026
1071
|
conditions.push("coalesce(t.archived, 0) = 0");
|
|
1027
1072
|
} else if (archiveStatus === "archived") {
|
|
1028
|
-
conditions.push("coalesce(t.archived, 0) <> 0");
|
|
1073
|
+
conditions.push(columns.has("archived") ? "coalesce(t.archived, 0) <> 0" : "0 = 1");
|
|
1029
1074
|
}
|
|
1030
1075
|
|
|
1031
|
-
if (!includeInternals) {
|
|
1076
|
+
if (!includeInternals && hasSpawnEdges) {
|
|
1032
1077
|
conditions.push(`not exists (
|
|
1033
1078
|
select 1 from thread_spawn_edges edge where edge.child_thread_id = t.id
|
|
1034
1079
|
)`);
|
|
1035
1080
|
}
|
|
1036
1081
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1082
|
+
const supportingColumns = ["title", "first_user_message"].filter((column) => columns.has(column));
|
|
1083
|
+
if (!includeSupporting && supportingColumns.length > 0) {
|
|
1084
|
+
conditions.push(`coalesce(${supportingColumns.map((column) => `nullif(trim(t.${column}), '')`).join(", ")}, '') not like ?`);
|
|
1039
1085
|
parameters.push(`${SUPPORTING_THREAD_PREFIX}%`);
|
|
1040
1086
|
}
|
|
1041
1087
|
|
|
1042
1088
|
if (Number.isFinite(inactiveBeforeMs) && inactiveBeforeMs > 0) {
|
|
1043
|
-
conditions.push(`${
|
|
1089
|
+
conditions.push(`${activity} > 0 and ${activity} <= ?`);
|
|
1044
1090
|
parameters.push(Math.trunc(inactiveBeforeMs));
|
|
1045
1091
|
}
|
|
1046
1092
|
|
|
1047
|
-
if (typeof workspace === "string") {
|
|
1093
|
+
if (typeof workspace === "string" && columns.has("cwd")) {
|
|
1048
1094
|
conditions.push("coalesce(t.cwd, '') = ?");
|
|
1049
1095
|
parameters.push(workspace);
|
|
1050
1096
|
}
|
|
@@ -1052,14 +1098,10 @@ function getSessionConditions({
|
|
|
1052
1098
|
const normalizedSearch = normalizeText(search).toLowerCase();
|
|
1053
1099
|
|
|
1054
1100
|
if (normalizedSearch) {
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
or instr(lower(coalesce(t.cwd, '')), ?) > 0
|
|
1060
|
-
or instr(lower(coalesce(t.rollout_path, '')), ?) > 0
|
|
1061
|
-
)`);
|
|
1062
|
-
parameters.push(...Array(5).fill(normalizedSearch));
|
|
1101
|
+
const searchColumns = ["id", "title", "first_user_message", "cwd", "rollout_path"]
|
|
1102
|
+
.filter((column) => column === "id" || columns.has(column));
|
|
1103
|
+
conditions.push(`(${searchColumns.map((column) => `instr(lower(coalesce(t.${column}, '')), ?) > 0`).join(" or ")})`);
|
|
1104
|
+
parameters.push(...Array(searchColumns.length).fill(normalizedSearch));
|
|
1063
1105
|
}
|
|
1064
1106
|
|
|
1065
1107
|
return {
|
|
@@ -1068,30 +1110,25 @@ function getSessionConditions({
|
|
|
1068
1110
|
};
|
|
1069
1111
|
}
|
|
1070
1112
|
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
t.
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
limit 1
|
|
1088
|
-
) as parent_thread_id
|
|
1089
|
-
`;
|
|
1090
|
-
|
|
1091
|
-
async function formatPagedThreadRows(stateDatabasePath, threadRows) {
|
|
1113
|
+
function sessionColumns(database) {
|
|
1114
|
+
const { activity, columns, created, hasSpawnEdges } = stateExpressions(database);
|
|
1115
|
+
const optional = OPTIONAL_THREAD_COLUMNS.map((column) =>
|
|
1116
|
+
`${columnExpression(columns, column)} as ${column}`,
|
|
1117
|
+
);
|
|
1118
|
+
const parent = hasSpawnEdges
|
|
1119
|
+
? `(select edge.parent_thread_id from thread_spawn_edges edge where edge.child_thread_id = t.id limit 1)`
|
|
1120
|
+
: "null";
|
|
1121
|
+
return `t.id, t.rollout_path, ${optional.join(", ")}, ${created} as created_at_ms_resolved, ${activity} as updated_at_ms_resolved, ${parent} as parent_thread_id`;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
async function formatPagedThreadRows(stateDatabase, threadRows) {
|
|
1125
|
+
const stateDatabasePath = typeof stateDatabase === "string" ? stateDatabase : stateDatabase.path;
|
|
1126
|
+
const hasSpawnEdges = typeof stateDatabase === "string"
|
|
1127
|
+
? inspectSqliteTable(stateDatabasePath, "thread_spawn_edges").exists
|
|
1128
|
+
: stateSchema(stateDatabase).hasSpawnEdges;
|
|
1092
1129
|
const childIdsByParentId = new Map();
|
|
1093
1130
|
|
|
1094
|
-
if (threadRows.length > 0) {
|
|
1131
|
+
if (threadRows.length > 0 && hasSpawnEdges) {
|
|
1095
1132
|
const ids = threadRows.map((row) => String(row.id));
|
|
1096
1133
|
|
|
1097
1134
|
for (const idBatch of batches(ids)) {
|
|
@@ -1139,7 +1176,7 @@ async function formatPagedThreadRows(stateDatabasePath, threadRows) {
|
|
|
1139
1176
|
agentRole: threadRow.agent_role ?? transcriptHeader?.agentRole ?? null,
|
|
1140
1177
|
archived: Boolean(threadRow.archived),
|
|
1141
1178
|
childThreadIds: [...(childIdsByParentId.get(threadRow.id) ?? [])].sort(),
|
|
1142
|
-
createdAtMs: toTimestampMs(threadRow.created_at_ms),
|
|
1179
|
+
createdAtMs: toTimestampMs(threadRow.created_at_ms_resolved ?? threadRow.created_at_ms),
|
|
1143
1180
|
cwd: threadRow.cwd ?? "",
|
|
1144
1181
|
displayName,
|
|
1145
1182
|
firstUserMessage: threadRow.first_user_message ?? "",
|
|
@@ -1156,7 +1193,8 @@ async function formatPagedThreadRows(stateDatabasePath, threadRows) {
|
|
|
1156
1193
|
title: threadRow.title ?? "",
|
|
1157
1194
|
titleSource: title ? "sqlite_title" : firstUserMessage ? "sqlite_first_user_message" : "fallback",
|
|
1158
1195
|
transcriptBytes,
|
|
1159
|
-
updatedAtMs: toTimestampMs(threadRow.updated_at_ms),
|
|
1196
|
+
updatedAtMs: toTimestampMs(threadRow.updated_at_ms_resolved ?? threadRow.updated_at_ms),
|
|
1197
|
+
_stateDatabasePath: stateDatabasePath,
|
|
1160
1198
|
});
|
|
1161
1199
|
}
|
|
1162
1200
|
|
|
@@ -1173,9 +1211,9 @@ async function getStoreAvailability(paths) {
|
|
|
1173
1211
|
] = await Promise.all([
|
|
1174
1212
|
pathExists(paths.desktopStatePath),
|
|
1175
1213
|
pathExists(paths.desktopStateBackupPath),
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1214
|
+
Promise.resolve(paths.goalsDatabasePaths.length > 0),
|
|
1215
|
+
Promise.resolve(paths.logsDatabasePaths.length > 0),
|
|
1216
|
+
Promise.resolve(paths.memoryDatabasePaths.length > 0),
|
|
1179
1217
|
]);
|
|
1180
1218
|
|
|
1181
1219
|
return {
|
|
@@ -1261,13 +1299,13 @@ export async function loadDeletionStore({ codexHome, recordIds }) {
|
|
|
1261
1299
|
|
|
1262
1300
|
for (let offset = 0; offset < pendingIds.length; offset += 400) {
|
|
1263
1301
|
const idBatch = pendingIds.slice(offset, offset + 400);
|
|
1264
|
-
const stateChildren =
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1302
|
+
const stateChildren = paths.stateDatabases.flatMap((database) => stateSchema(database).hasSpawnEdges
|
|
1303
|
+
? queryRows(
|
|
1304
|
+
database.path,
|
|
1305
|
+
`select parent_thread_id, child_thread_id from thread_spawn_edges where parent_thread_id in (${placeholders(idBatch)})`,
|
|
1306
|
+
idBatch,
|
|
1307
|
+
)
|
|
1308
|
+
: []);
|
|
1271
1309
|
|
|
1272
1310
|
for (const edge of stateChildren) {
|
|
1273
1311
|
const childId = String(edge.child_thread_id);
|
|
@@ -1288,19 +1326,29 @@ export async function loadDeletionStore({ codexHome, recordIds }) {
|
|
|
1288
1326
|
}
|
|
1289
1327
|
|
|
1290
1328
|
const ids = [...selectedIds];
|
|
1291
|
-
const
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1329
|
+
const threadRowsById = new Map();
|
|
1330
|
+
for (const database of paths.stateDatabases) {
|
|
1331
|
+
for (const idBatch of batches(ids)) {
|
|
1332
|
+
for (const row of queryRows(
|
|
1333
|
+
database.path,
|
|
1334
|
+
`select ${sessionColumns(database)} from threads t where t.id in (${placeholders(idBatch)})`,
|
|
1335
|
+
idBatch,
|
|
1336
|
+
)) {
|
|
1337
|
+
const id = String(row.id);
|
|
1338
|
+
if (!threadRowsById.has(id)) threadRowsById.set(id, { ...row, _database: database });
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1301
1341
|
}
|
|
1302
|
-
|
|
1303
|
-
const
|
|
1342
|
+
const threadRows = [...threadRowsById.values()];
|
|
1343
|
+
const spawnEdgesByKey = new Map();
|
|
1344
|
+
for (const database of paths.stateDatabases) {
|
|
1345
|
+
if (!stateSchema(database).hasSpawnEdges) continue;
|
|
1346
|
+
for (const edge of queryRelatedSpawnEdges(database.path, ids)) {
|
|
1347
|
+
const key = `${edge.parent_thread_id}\0${edge.child_thread_id}`;
|
|
1348
|
+
if (!spawnEdgesByKey.has(key)) spawnEdgesByKey.set(key, edge);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
const spawnEdges = [...spawnEdgesByKey.values()];
|
|
1304
1352
|
const childIdsByParentId = new Map();
|
|
1305
1353
|
const parentIdsByChildId = new Map();
|
|
1306
1354
|
|
|
@@ -1325,7 +1373,13 @@ export async function loadDeletionStore({ codexHome, recordIds }) {
|
|
|
1325
1373
|
}
|
|
1326
1374
|
}
|
|
1327
1375
|
|
|
1328
|
-
const formattedRows =
|
|
1376
|
+
const formattedRows = [];
|
|
1377
|
+
for (const database of paths.stateDatabases) {
|
|
1378
|
+
formattedRows.push(...await formatPagedThreadRows(
|
|
1379
|
+
database,
|
|
1380
|
+
threadRows.filter((row) => row._database === database),
|
|
1381
|
+
));
|
|
1382
|
+
}
|
|
1329
1383
|
const recordsById = new Map();
|
|
1330
1384
|
|
|
1331
1385
|
for (const record of formattedRows) {
|
|
@@ -1427,6 +1481,12 @@ export async function loadDeletionStore({ codexHome, recordIds }) {
|
|
|
1427
1481
|
sessionIndexPath: paths.sessionIndexPath,
|
|
1428
1482
|
spawnEdges,
|
|
1429
1483
|
stateDatabasePath: paths.stateDatabasePath,
|
|
1484
|
+
stateDatabasePaths: paths.stateDatabasePaths,
|
|
1485
|
+
stateDatabases: paths.stateDatabases,
|
|
1486
|
+
resolvedDatabases: paths.resolvedDatabases,
|
|
1487
|
+
logsDatabasePaths: paths.logsDatabasePaths,
|
|
1488
|
+
memoryDatabasePaths: paths.memoryDatabasePaths,
|
|
1489
|
+
goalsDatabasePaths: paths.goalsDatabasePaths,
|
|
1430
1490
|
transcriptHeaders: new Map(
|
|
1431
1491
|
[...transcriptHeaders].filter(([id]) => relevantIds.has(id)),
|
|
1432
1492
|
),
|
|
@@ -1445,80 +1505,109 @@ export async function listSessions({
|
|
|
1445
1505
|
search = "",
|
|
1446
1506
|
sort = "updated",
|
|
1447
1507
|
workspace,
|
|
1508
|
+
forceUnion = false,
|
|
1448
1509
|
}) {
|
|
1449
|
-
const paths = getCodexPaths(codexHome);
|
|
1510
|
+
const paths = getCodexPaths(codexHome, { refresh });
|
|
1450
1511
|
const boundedPageSize = Number.isFinite(pageSize)
|
|
1451
1512
|
? Math.min(MAX_PAGE_SIZE, Math.max(1, Math.trunc(pageSize)))
|
|
1452
1513
|
: DEFAULT_PAGE_SIZE;
|
|
1453
1514
|
const requestedPage = Number.isFinite(page) ? Math.max(1, Math.trunc(page)) : 1;
|
|
1454
1515
|
const resolvedSort = SESSION_SORTS.has(sort) ? sort : "updated";
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
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}`,
|
|
1516
|
+
if (paths.stateDatabases.length === 1 && resolvedSort !== "size" && !forceUnion) {
|
|
1517
|
+
const database = paths.stateDatabases[0];
|
|
1518
|
+
const conditions = getSessionConditions(database, {
|
|
1519
|
+
archiveStatus, inactiveBeforeMs, includeInternals, includeSupporting, search, workspace,
|
|
1520
|
+
});
|
|
1521
|
+
const countRow = queryRows(
|
|
1522
|
+
database.path,
|
|
1523
|
+
`select count(*) as count from threads t ${conditions.sql}`,
|
|
1468
1524
|
conditions.parameters,
|
|
1469
|
-
)
|
|
1470
|
-
|
|
1471
|
-
const total = matchingIds.length;
|
|
1525
|
+
)[0];
|
|
1526
|
+
const total = Number(countRow?.count ?? 0);
|
|
1472
1527
|
const pageCount = Math.max(1, Math.ceil(total / boundedPageSize));
|
|
1473
1528
|
const currentPage = Math.min(requestedPage, pageCount);
|
|
1474
|
-
const
|
|
1475
|
-
|
|
1476
|
-
|
|
1529
|
+
const rows = queryRows(
|
|
1530
|
+
database.path,
|
|
1531
|
+
`select ${sessionColumns(database)}
|
|
1532
|
+
from threads t
|
|
1533
|
+
${conditions.sql}
|
|
1534
|
+
order by ${getSessionOrder(resolvedSort, database)}
|
|
1535
|
+
limit ? offset ?`,
|
|
1536
|
+
[...conditions.parameters, boundedPageSize, (currentPage - 1) * boundedPageSize],
|
|
1477
1537
|
);
|
|
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
1538
|
|
|
1490
1539
|
return {
|
|
1491
1540
|
page: currentPage,
|
|
1492
1541
|
pageCount,
|
|
1493
1542
|
pageSize: boundedPageSize,
|
|
1494
|
-
records: await formatPagedThreadRows(
|
|
1543
|
+
records: await formatPagedThreadRows(database, rows),
|
|
1495
1544
|
total,
|
|
1496
1545
|
};
|
|
1497
1546
|
}
|
|
1498
1547
|
|
|
1499
|
-
const
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
)
|
|
1504
|
-
|
|
1548
|
+
const ordered = [];
|
|
1549
|
+
const seenIds = paths.stateDatabases.length > 1 ? new Set() : null;
|
|
1550
|
+
const compactSizeIds = resolvedSort === "size" && paths.stateDatabases.length === 1;
|
|
1551
|
+
let compareSortRows = null;
|
|
1552
|
+
for (const database of paths.stateDatabases) {
|
|
1553
|
+
const conditions = getSessionConditions(database, {
|
|
1554
|
+
archiveStatus, inactiveBeforeMs, includeInternals, includeSupporting, search, workspace,
|
|
1555
|
+
});
|
|
1556
|
+
const sessionSort = resolvedSort === "size" ? null : getSessionSort(resolvedSort, database);
|
|
1557
|
+
compareSortRows ??= sessionSort?.compare ?? null;
|
|
1558
|
+
const rows = queryRows(
|
|
1559
|
+
database.path,
|
|
1560
|
+
`select t.id${sessionSort ? `, ${sessionSort.selection}` : ""} from threads t ${conditions.sql}`,
|
|
1561
|
+
conditions.parameters,
|
|
1562
|
+
);
|
|
1563
|
+
for (const row of rows) {
|
|
1564
|
+
const id = String(row.id);
|
|
1565
|
+
if (seenIds?.has(id)) continue;
|
|
1566
|
+
seenIds?.add(id);
|
|
1567
|
+
if (compactSizeIds) {
|
|
1568
|
+
ordered.push(id);
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
row.database = database;
|
|
1572
|
+
row.id = id;
|
|
1573
|
+
ordered.push(row);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
if (resolvedSort === "size") {
|
|
1577
|
+
const sizes = await getSessionSizeIndex(paths, { refresh });
|
|
1578
|
+
ordered.sort((left, right) => compareSessionIdsBySize(
|
|
1579
|
+
compactSizeIds ? left : left.id,
|
|
1580
|
+
compactSizeIds ? right : right.id,
|
|
1581
|
+
sizes,
|
|
1582
|
+
));
|
|
1583
|
+
} else {
|
|
1584
|
+
ordered.sort((left, right) => compareSortRows(left, right) || left.id.localeCompare(right.id));
|
|
1585
|
+
}
|
|
1586
|
+
const total = ordered.length;
|
|
1505
1587
|
const pageCount = Math.max(1, Math.ceil(total / boundedPageSize));
|
|
1506
1588
|
const currentPage = Math.min(requestedPage, pageCount);
|
|
1507
|
-
const
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1589
|
+
const pageItems = ordered.slice((currentPage - 1) * boundedPageSize, currentPage * boundedPageSize);
|
|
1590
|
+
const recordsById = new Map();
|
|
1591
|
+
const itemsByDatabase = new Map();
|
|
1592
|
+
if (compactSizeIds) {
|
|
1593
|
+
itemsByDatabase.set(paths.stateDatabases[0], pageItems);
|
|
1594
|
+
} else {
|
|
1595
|
+
for (const item of pageItems) {
|
|
1596
|
+
const values = itemsByDatabase.get(item.database) ?? [];
|
|
1597
|
+
values.push(item.id);
|
|
1598
|
+
itemsByDatabase.set(item.database, values);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
for (const [database, ids] of itemsByDatabase) {
|
|
1602
|
+
const rows = queryRows(database.path, `select ${sessionColumns(database)} from threads t where t.id in (${placeholders(ids)})`, ids);
|
|
1603
|
+
for (const record of await formatPagedThreadRows(database, rows)) recordsById.set(record.id, record);
|
|
1604
|
+
}
|
|
1516
1605
|
|
|
1517
1606
|
return {
|
|
1518
1607
|
page: currentPage,
|
|
1519
1608
|
pageCount,
|
|
1520
1609
|
pageSize: boundedPageSize,
|
|
1521
|
-
records:
|
|
1610
|
+
records: pageItems.map((item) => recordsById.get(compactSizeIds ? item : item.id)).filter(Boolean),
|
|
1522
1611
|
total,
|
|
1523
1612
|
};
|
|
1524
1613
|
}
|
|
@@ -1573,47 +1662,41 @@ async function measureTranscriptStorage(transcriptDirectories) {
|
|
|
1573
1662
|
}
|
|
1574
1663
|
|
|
1575
1664
|
export async function getSessionOverview({ codexHome, refresh = false }) {
|
|
1576
|
-
const paths = getCodexPaths(codexHome);
|
|
1577
|
-
const
|
|
1578
|
-
const
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
coalesce(sum(case when ${SESSION_ACTIVITY_SQL} = 0 then 1 else 0 end), 0) as unknown_activity_count
|
|
1596
|
-
from threads t
|
|
1597
|
-
left join (
|
|
1598
|
-
select distinct child_thread_id from thread_spawn_edges
|
|
1599
|
-
) children on children.child_thread_id = t.id`,
|
|
1600
|
-
[supportingPattern, supportingPattern],
|
|
1601
|
-
)[0] ?? {};
|
|
1602
|
-
const workspaceSessionRows = queryRows(
|
|
1603
|
-
paths.stateDatabasePath,
|
|
1604
|
-
`select
|
|
1605
|
-
t.id,
|
|
1606
|
-
coalesce(t.cwd, '') as path,
|
|
1607
|
-
${SESSION_ACTIVITY_SQL} as last_activity_at_ms
|
|
1608
|
-
from threads t`,
|
|
1609
|
-
);
|
|
1665
|
+
const paths = getCodexPaths(codexHome, { refresh });
|
|
1666
|
+
const sessions = new Map();
|
|
1667
|
+
for (const database of paths.stateDatabases) {
|
|
1668
|
+
const { activity, columns, hasSpawnEdges } = stateExpressions(database);
|
|
1669
|
+
const projection = [
|
|
1670
|
+
"t.id",
|
|
1671
|
+
`${columns.has("cwd") ? "coalesce(t.cwd, '')" : "''"} as path`,
|
|
1672
|
+
`${columns.has("archived") ? "coalesce(t.archived, 0)" : "0"} as archived`,
|
|
1673
|
+
`${columns.has("title") ? "coalesce(t.title, '')" : "''"} as title`,
|
|
1674
|
+
`${columns.has("first_user_message") ? "coalesce(t.first_user_message, '')" : "''"} as first_user_message`,
|
|
1675
|
+
`${activity} as last_activity_at_ms`,
|
|
1676
|
+
`${hasSpawnEdges ? "exists(select 1 from thread_spawn_edges edge where edge.child_thread_id = t.id)" : "0"} as is_subagent`,
|
|
1677
|
+
].join(", ");
|
|
1678
|
+
for (const row of queryRows(database.path, `select ${projection} from threads t`)) {
|
|
1679
|
+
const id = String(row.id);
|
|
1680
|
+
if (!sessions.has(id)) sessions.set(id, row);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
const workspaceSessionRows = [...sessions.values()];
|
|
1610
1684
|
const [sizes, storage] = await Promise.all([
|
|
1611
1685
|
getSessionSizeIndex(paths, { refresh }),
|
|
1612
1686
|
measureTranscriptStorage(paths.transcriptDirectories),
|
|
1613
1687
|
]);
|
|
1614
1688
|
const workspaces = new Map();
|
|
1689
|
+
let archivedSessionCount = 0;
|
|
1690
|
+
let subagentCount = 0;
|
|
1691
|
+
let supportingCount = 0;
|
|
1692
|
+
let unknownActivityCount = 0;
|
|
1615
1693
|
|
|
1616
1694
|
for (const row of workspaceSessionRows) {
|
|
1695
|
+
const supporting = `${row.title || row.first_user_message || ""}`.startsWith(SUPPORTING_THREAD_PREFIX);
|
|
1696
|
+
archivedSessionCount += row.archived ? 1 : 0;
|
|
1697
|
+
subagentCount += row.is_subagent ? 1 : 0;
|
|
1698
|
+
supportingCount += supporting ? 1 : 0;
|
|
1699
|
+
unknownActivityCount += Number(row.last_activity_at_ms) ? 0 : 1;
|
|
1617
1700
|
const workspacePath = row.path ?? "";
|
|
1618
1701
|
const current = workspaces.get(workspacePath) ?? {
|
|
1619
1702
|
lastActivityAtMs: 0,
|
|
@@ -1632,14 +1715,14 @@ export async function getSessionOverview({ codexHome, refresh = false }) {
|
|
|
1632
1715
|
}
|
|
1633
1716
|
|
|
1634
1717
|
return {
|
|
1635
|
-
activeSessionCount:
|
|
1636
|
-
archivedSessionCount
|
|
1718
|
+
activeSessionCount: sessions.size - archivedSessionCount,
|
|
1719
|
+
archivedSessionCount,
|
|
1637
1720
|
calculatedAtMs: Date.now(),
|
|
1638
|
-
primarySessionCount:
|
|
1639
|
-
sessionCount:
|
|
1640
|
-
subagentCount
|
|
1641
|
-
supportingCount
|
|
1642
|
-
unknownActivityCount
|
|
1721
|
+
primarySessionCount: sessions.size - subagentCount - supportingCount,
|
|
1722
|
+
sessionCount: sessions.size,
|
|
1723
|
+
subagentCount,
|
|
1724
|
+
supportingCount,
|
|
1725
|
+
unknownActivityCount,
|
|
1643
1726
|
workspaces: [...workspaces.values()].sort((left, right) =>
|
|
1644
1727
|
Number(!left.path) - Number(!right.path)
|
|
1645
1728
|
|| right.lastActivityAtMs - left.lastActivityAtMs
|
|
@@ -1650,13 +1733,11 @@ export async function getSessionOverview({ codexHome, refresh = false }) {
|
|
|
1650
1733
|
|
|
1651
1734
|
export async function getSessionRecord({ codexHome, id }) {
|
|
1652
1735
|
const paths = getCodexPaths(codexHome);
|
|
1653
|
-
const
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
const records = await formatPagedThreadRows(paths.stateDatabasePath, rows);
|
|
1659
|
-
return records[0] ?? null;
|
|
1736
|
+
for (const database of paths.stateDatabases) {
|
|
1737
|
+
const rows = queryRows(database.path, `select ${sessionColumns(database)} from threads t where t.id = ? limit 1`, [id]);
|
|
1738
|
+
if (rows.length) return (await formatPagedThreadRows(database, rows))[0] ?? null;
|
|
1739
|
+
}
|
|
1740
|
+
return null;
|
|
1660
1741
|
}
|
|
1661
1742
|
|
|
1662
1743
|
export function filterAndSortSessions({
|
|
@@ -1753,6 +1834,18 @@ function findRowsForIds(databasePath, tableName, columnName, ids, { limitOne = f
|
|
|
1753
1834
|
return rows;
|
|
1754
1835
|
}
|
|
1755
1836
|
|
|
1837
|
+
function fingerprintRowsForIds(hash, databasePath, tableName, columnName, ids) {
|
|
1838
|
+
for (const idBatch of batches(ids)) {
|
|
1839
|
+
const rows = queryRows(
|
|
1840
|
+
databasePath,
|
|
1841
|
+
`select * from ${tableName} where ${columnName} in (${placeholders(idBatch)})`,
|
|
1842
|
+
idBatch,
|
|
1843
|
+
);
|
|
1844
|
+
rows.sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
1845
|
+
hash.update(`${databasePath}\0${tableName}\0${JSON.stringify(rows)}\0`);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1756
1849
|
function* deleteStatements(tableName, columnName, ids) {
|
|
1757
1850
|
for (const idBatch of batches(ids)) {
|
|
1758
1851
|
yield {
|
|
@@ -1842,15 +1935,16 @@ export async function planSessionDeletion({ recordIds, store }) {
|
|
|
1842
1935
|
|
|
1843
1936
|
return count;
|
|
1844
1937
|
}, 0);
|
|
1845
|
-
const logRowCount = store.
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
const
|
|
1852
|
-
|
|
1853
|
-
:
|
|
1938
|
+
const logRowCount = (store.logsDatabasePaths ?? [store.logsDatabasePath].filter(Boolean))
|
|
1939
|
+
.reduce((total, databasePath) => total + countRowsForIds(databasePath, "logs", "thread_id", deletionIds), 0);
|
|
1940
|
+
const memoryRowCount = (store.memoryDatabasePaths ?? [store.memoryDatabasePath].filter(Boolean))
|
|
1941
|
+
.reduce((total, databasePath) => total + countRowsForIds(databasePath, "stage1_outputs", "thread_id", deletionIds), 0);
|
|
1942
|
+
const goalRowCount = (store.goalsDatabasePaths ?? [store.goalsDatabasePath].filter(Boolean))
|
|
1943
|
+
.reduce((total, databasePath) => total + countRowsForIds(databasePath, "thread_goals", "thread_id", deletionIds), 0);
|
|
1944
|
+
const stateTargets = (store.stateDatabases ?? [{ path: store.stateDatabasePath }]).map((database) => ({
|
|
1945
|
+
database,
|
|
1946
|
+
ids: findRowsForIds(database.path, "threads", "id", deletionIds).map((row) => String(row.id)),
|
|
1947
|
+
}));
|
|
1854
1948
|
|
|
1855
1949
|
return {
|
|
1856
1950
|
childCount: Math.max(0, deletionIds.length - recordIds.length),
|
|
@@ -1869,6 +1963,7 @@ export async function planSessionDeletion({ recordIds, store }) {
|
|
|
1869
1963
|
records: selectedRecords,
|
|
1870
1964
|
sessionIndexMatchCount: sessionIndexMatches.count,
|
|
1871
1965
|
spawnEdgeCount,
|
|
1966
|
+
stateTargets,
|
|
1872
1967
|
transcriptBytes,
|
|
1873
1968
|
transcriptFileCount,
|
|
1874
1969
|
transcriptPaths,
|
|
@@ -1880,10 +1975,10 @@ const BACKUP_RESERVE_RATIO = 0.05;
|
|
|
1880
1975
|
|
|
1881
1976
|
function getBackupSourcePaths({ plan, scope, store }) {
|
|
1882
1977
|
const databasePaths = [
|
|
1883
|
-
store.stateDatabasePath,
|
|
1884
|
-
store.
|
|
1885
|
-
scope === "deep"
|
|
1886
|
-
scope === "deep"
|
|
1978
|
+
...(store.stateDatabasePaths ?? [store.stateDatabasePath]),
|
|
1979
|
+
...(store.logsDatabasePaths ?? [store.logsDatabasePath].filter(Boolean)),
|
|
1980
|
+
...(scope === "deep" ? (store.memoryDatabasePaths ?? [store.memoryDatabasePath].filter(Boolean)) : []),
|
|
1981
|
+
...(scope === "deep" ? (store.goalsDatabasePaths ?? [store.goalsDatabasePath].filter(Boolean)) : []),
|
|
1887
1982
|
].filter(Boolean);
|
|
1888
1983
|
|
|
1889
1984
|
return [
|
|
@@ -1937,6 +2032,7 @@ export async function fingerprintSessionDeletion({ plan, scope, store }) {
|
|
|
1937
2032
|
store.hasMemoryDatabase,
|
|
1938
2033
|
] : []),
|
|
1939
2034
|
].map(Number).join("\0")}\0`);
|
|
2035
|
+
hash.update(`resolved-databases\0${JSON.stringify(store.resolvedDatabases ?? {})}\0`);
|
|
1940
2036
|
|
|
1941
2037
|
for (const record of [...plan.records].sort((left, right) => left.id.localeCompare(right.id))) {
|
|
1942
2038
|
hash.update([
|
|
@@ -1955,6 +2051,26 @@ export async function fingerprintSessionDeletion({ plan, scope, store }) {
|
|
|
1955
2051
|
hash.update(`id\0${id}\0`);
|
|
1956
2052
|
}
|
|
1957
2053
|
|
|
2054
|
+
for (const database of store.stateDatabases ?? [{ path: store.stateDatabasePath }]) {
|
|
2055
|
+
fingerprintRowsForIds(hash, database.path, "threads", "id", plan.ids);
|
|
2056
|
+
if (stateSchema(database).hasSpawnEdges) {
|
|
2057
|
+
fingerprintRowsForIds(hash, database.path, "thread_spawn_edges", "parent_thread_id", plan.ids);
|
|
2058
|
+
fingerprintRowsForIds(hash, database.path, "thread_spawn_edges", "child_thread_id", plan.ids);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
for (const databasePath of store.logsDatabasePaths ?? [store.logsDatabasePath].filter(Boolean)) {
|
|
2062
|
+
fingerprintRowsForIds(hash, databasePath, "logs", "thread_id", plan.ids);
|
|
2063
|
+
}
|
|
2064
|
+
if (scope === "deep") {
|
|
2065
|
+
for (const databasePath of store.memoryDatabasePaths ?? [store.memoryDatabasePath].filter(Boolean)) {
|
|
2066
|
+
fingerprintRowsForIds(hash, databasePath, "stage1_outputs", "thread_id", plan.ids);
|
|
2067
|
+
}
|
|
2068
|
+
for (const databasePath of store.goalsDatabasePaths ?? [store.goalsDatabasePath].filter(Boolean)) {
|
|
2069
|
+
fingerprintRowsForIds(hash, databasePath, "thread_goals", "thread_id", plan.ids);
|
|
2070
|
+
fingerprintRowsForIds(hash, databasePath, "thread_goal_continuation_deferrals", "thread_id", plan.ids);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
|
|
1958
2074
|
for (const filePath of [...plan.missingTranscriptPaths].sort()) {
|
|
1959
2075
|
hash.update(`missing-transcript\0${filePath}\0`);
|
|
1960
2076
|
}
|
|
@@ -2123,10 +2239,10 @@ async function createOperationBackup({ onProgress, plan, scope, store }) {
|
|
|
2123
2239
|
transcriptNameCounts.set(name, (transcriptNameCounts.get(name) ?? 0) + 1);
|
|
2124
2240
|
}
|
|
2125
2241
|
const snapshotCandidates = [
|
|
2126
|
-
[store.stateDatabasePath,
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2242
|
+
...(store.stateDatabasePaths ?? [store.stateDatabasePath]).map((databasePath) => [databasePath, path.basename(databasePath), true]),
|
|
2243
|
+
...(store.logsDatabasePaths ?? [store.logsDatabasePath].filter(Boolean)).map((databasePath) => [databasePath, path.basename(databasePath), true]),
|
|
2244
|
+
...(scope === "deep" ? (store.memoryDatabasePaths ?? [store.memoryDatabasePath].filter(Boolean)) : []).map((databasePath) => [databasePath, path.basename(databasePath), true]),
|
|
2245
|
+
...(scope === "deep" ? (store.goalsDatabasePaths ?? [store.goalsDatabasePath].filter(Boolean)) : []).map((databasePath) => [databasePath, path.basename(databasePath), true]),
|
|
2130
2246
|
];
|
|
2131
2247
|
const totalItems = backupFiles.length + plan.transcriptPaths.length + snapshotCandidates.length;
|
|
2132
2248
|
let completedItems = 0;
|
|
@@ -2199,7 +2315,18 @@ async function createOperationBackup({ onProgress, plan, scope, store }) {
|
|
|
2199
2315
|
|
|
2200
2316
|
await atomicWriteFile(
|
|
2201
2317
|
path.join(backupDirectory, "operation.json"),
|
|
2202
|
-
`${JSON.stringify({
|
|
2318
|
+
`${JSON.stringify({
|
|
2319
|
+
version: 3,
|
|
2320
|
+
ids: plan.ids,
|
|
2321
|
+
scope,
|
|
2322
|
+
createdAtMs: Date.now(),
|
|
2323
|
+
copiedFiles,
|
|
2324
|
+
databaseSnapshots,
|
|
2325
|
+
files,
|
|
2326
|
+
profileId: CODEX_DATABASE_PROFILE.id,
|
|
2327
|
+
resolvedDatabases: store.resolvedDatabases ?? {},
|
|
2328
|
+
compatibilityStatus: (await diagnoseStorageCompatibility({ codexHome: store.codexHome })).status,
|
|
2329
|
+
}, null, 2)}\n`,
|
|
2203
2330
|
);
|
|
2204
2331
|
|
|
2205
2332
|
return backupDirectory;
|
|
@@ -2266,39 +2393,40 @@ export async function executeSessionDeletion({
|
|
|
2266
2393
|
|
|
2267
2394
|
try {
|
|
2268
2395
|
if (scope === "deep" && store.hasMemoryDatabase) {
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
);
|
|
2396
|
+
for (const databasePath of store.memoryDatabasePaths ?? [store.memoryDatabasePath]) {
|
|
2397
|
+
executeTransaction(databasePath, deleteStatements("stage1_outputs", "thread_id", plan.ids));
|
|
2398
|
+
}
|
|
2273
2399
|
}
|
|
2274
2400
|
if (scope === "deep" && store.hasGoalsDatabase) {
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2401
|
+
for (const databasePath of store.goalsDatabasePaths ?? [store.goalsDatabasePath]) {
|
|
2402
|
+
executeTransaction(databasePath, [
|
|
2403
|
+
...deleteStatements("thread_goal_continuation_deferrals", "thread_id", plan.ids),
|
|
2404
|
+
...deleteStatements("thread_goals", "thread_id", plan.ids),
|
|
2405
|
+
]);
|
|
2406
|
+
}
|
|
2279
2407
|
}
|
|
2280
2408
|
|
|
2281
2409
|
if (store.hasLogsDatabase) {
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
);
|
|
2410
|
+
for (const databasePath of store.logsDatabasePaths ?? [store.logsDatabasePath]) {
|
|
2411
|
+
executeTransaction(databasePath, deleteStatements("logs", "thread_id", plan.ids));
|
|
2412
|
+
}
|
|
2286
2413
|
}
|
|
2287
|
-
const
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
const
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2414
|
+
for (const { database, ids } of plan.stateTargets ?? (store.stateDatabases ?? [{ path: store.stateDatabasePath }]).map((database) => ({ database, ids: plan.ids }))) {
|
|
2415
|
+
const stateStatements = [];
|
|
2416
|
+
const hasSpawnEdges = stateSchema(database).hasSpawnEdges;
|
|
2417
|
+
for (const idBatch of batches(ids)) {
|
|
2418
|
+
const idPlaceholders = placeholders(idBatch);
|
|
2419
|
+
if (hasSpawnEdges) stateStatements.push({
|
|
2420
|
+
parameters: [...idBatch, ...idBatch],
|
|
2421
|
+
sql: `delete from thread_spawn_edges where parent_thread_id in (${idPlaceholders}) or child_thread_id in (${idPlaceholders})`,
|
|
2422
|
+
});
|
|
2423
|
+
stateStatements.push({
|
|
2424
|
+
parameters: idBatch,
|
|
2425
|
+
sql: `delete from threads where id in (${idPlaceholders})`,
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2428
|
+
executeTransaction(database.path, stateStatements);
|
|
2299
2429
|
}
|
|
2300
|
-
|
|
2301
|
-
executeTransaction(store.stateDatabasePath, stateStatements);
|
|
2302
2430
|
await reportProgress(onProgress, {
|
|
2303
2431
|
canCancel: false,
|
|
2304
2432
|
message: "Updating session records",
|
|
@@ -2434,7 +2562,7 @@ export async function listSessionDeletionBackups({ codexHome }) {
|
|
|
2434
2562
|
fs.stat(backupDirectory),
|
|
2435
2563
|
measurePath(backupDirectory),
|
|
2436
2564
|
]);
|
|
2437
|
-
const restorable = operation?.version
|
|
2565
|
+
const restorable = [2, 3].includes(operation?.version) &&
|
|
2438
2566
|
Array.isArray(operation.files) &&
|
|
2439
2567
|
operation.files.length > 0;
|
|
2440
2568
|
|
|
@@ -2484,9 +2612,10 @@ export async function restoreSessionDeletionBackup({
|
|
|
2484
2612
|
const operationPath = path.join(resolvedBackupDirectory, "operation.json");
|
|
2485
2613
|
const operation = await readJsonFile(operationPath);
|
|
2486
2614
|
|
|
2487
|
-
if (operation?.version
|
|
2615
|
+
if (![2, 3].includes(operation?.version) || !Array.isArray(operation.files) || operation.files.length === 0) {
|
|
2488
2616
|
throw new Error("This backup cannot be restored automatically. Its files are still available for manual recovery.");
|
|
2489
2617
|
}
|
|
2618
|
+
const currentDatabasesBeforeRestore = getCodexPaths(codexHome, { refresh: true }).resolvedDatabases;
|
|
2490
2619
|
|
|
2491
2620
|
const files = operation.files.map((entry) => {
|
|
2492
2621
|
if (typeof entry?.backupPath !== "string" || typeof entry?.originalPath !== "string") {
|
|
@@ -2599,7 +2728,10 @@ export async function restoreSessionDeletionBackup({
|
|
|
2599
2728
|
progress: 100,
|
|
2600
2729
|
});
|
|
2601
2730
|
|
|
2731
|
+
const layoutChanged = operation.version === 3
|
|
2732
|
+
&& JSON.stringify(operation.resolvedDatabases ?? {}) !== JSON.stringify(currentDatabasesBeforeRestore);
|
|
2602
2733
|
return {
|
|
2734
|
+
note: layoutChanged ? "The Codex storage layout changed after this backup was created. The original files were restored to their recorded locations." : null,
|
|
2603
2735
|
restoredFileCount: files.length,
|
|
2604
2736
|
safetyBackupDirectory,
|
|
2605
2737
|
};
|
|
@@ -2615,17 +2747,22 @@ export async function restoreSessionDeletionBackup({
|
|
|
2615
2747
|
|
|
2616
2748
|
export async function verifySessionDeletion({ plan, scope = "deep", store }) {
|
|
2617
2749
|
const deletedIdSet = new Set(plan.ids);
|
|
2618
|
-
const remainingThreads =
|
|
2619
|
-
|
|
2620
|
-
);
|
|
2750
|
+
const remainingThreads = (store.stateDatabasePaths ?? [store.stateDatabasePath])
|
|
2751
|
+
.flatMap((databasePath) => findRowsForIds(databasePath, "threads", "id", plan.ids));
|
|
2621
2752
|
const remainingMemoryRecords = scope === "deep" && store.hasMemoryDatabase
|
|
2622
|
-
?
|
|
2753
|
+
? (store.memoryDatabasePaths ?? [store.memoryDatabasePath]).flatMap((databasePath) =>
|
|
2754
|
+
findRowsForIds(databasePath, "stage1_outputs", "thread_id", plan.ids))
|
|
2623
2755
|
: [];
|
|
2624
2756
|
const remainingGoalRecords = scope === "deep" && store.hasGoalsDatabase
|
|
2625
|
-
?
|
|
2757
|
+
? (store.goalsDatabasePaths ?? [store.goalsDatabasePath]).flatMap((databasePath) =>
|
|
2758
|
+
[
|
|
2759
|
+
...findRowsForIds(databasePath, "thread_goals", "thread_id", plan.ids),
|
|
2760
|
+
...findRowsForIds(databasePath, "thread_goal_continuation_deferrals", "thread_id", plan.ids),
|
|
2761
|
+
])
|
|
2626
2762
|
: [];
|
|
2627
2763
|
const remainingLogRecords = store.hasLogsDatabase
|
|
2628
|
-
?
|
|
2764
|
+
? (store.logsDatabasePaths ?? [store.logsDatabasePath]).flatMap((databasePath) =>
|
|
2765
|
+
findRowsForIds(databasePath, "logs", "thread_id", plan.ids, { limitOne: true }))
|
|
2629
2766
|
: [];
|
|
2630
2767
|
const [sessionIndexMatches, historyMatches] = await Promise.all([
|
|
2631
2768
|
inspectJsonlMatches(
|