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.
@@ -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(expandHome(claudeHomeInput || process.env.CLAUDE_CONFIG_DIR || "~/.claude"));
42
+ const claudeHome = path.resolve(expandHomePath(claudeHomeInput || process.env.CLAUDE_CONFIG_DIR || "~/.claude"));
45
43
  const desktopDataHome = desktopDataHomeInput
46
- ? path.resolve(expandHome(desktopDataHomeInput))
47
- : process.platform === "darwin"
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(expandHome(codexHomeInput || "~/.codex"));
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(expandHome(codexHome || "~/.codex"));
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
- return execFileSync(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
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
  }
@@ -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 = expandHome(value.trim());
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
- const xdgConfigHome = process.env.XDG_CONFIG_HOME;
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) {