cogpit-memory 0.1.5 → 0.1.8

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.
Files changed (3) hide show
  1. package/dist/cli.js +674 -95
  2. package/dist/index.js +686 -95
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -73,6 +73,9 @@ function isSystemMessage(msg) {
73
73
  function isSummaryMessage(msg) {
74
74
  return msg.type === "summary";
75
75
  }
76
+ function isCompactBoundary(msg) {
77
+ return msg.type === "system" && msg.subtype === "compact_boundary";
78
+ }
76
79
  function extractTextFromContent(content) {
77
80
  if (typeof content === "string") return content;
78
81
  return content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
@@ -152,7 +155,7 @@ function buildTurns(messages) {
152
155
  agentBlockMap.set(parentId, block);
153
156
  }
154
157
  }
155
- function finalizeTurn() {
158
+ function finalizeTurn2() {
156
159
  if (!current) return;
157
160
  for (const tc of current.toolCalls) {
158
161
  flushSubAgentMessages(tc.id);
@@ -166,13 +169,21 @@ function buildTurns(messages) {
166
169
  }
167
170
  for (const msg of messages) {
168
171
  if (isSummaryMessage(msg)) {
169
- finalizeTurn();
172
+ finalizeTurn2();
170
173
  pendingCompaction = buildCompactionSummary(
171
174
  turns,
172
175
  msg.summary ?? "Conversation compacted"
173
176
  );
174
177
  continue;
175
178
  }
179
+ if (isCompactBoundary(msg)) {
180
+ finalizeTurn2();
181
+ pendingCompaction = buildCompactionSummary(
182
+ turns,
183
+ msg.content ?? "Conversation compacted"
184
+ );
185
+ continue;
186
+ }
176
187
  if (isUserMessage(msg) && !msg.isMeta) {
177
188
  const content = msg.message.content;
178
189
  if (typeof content !== "string" && Array.isArray(content)) {
@@ -234,7 +245,7 @@ function buildTurns(messages) {
234
245
  continue;
235
246
  }
236
247
  }
237
- finalizeTurn();
248
+ finalizeTurn2();
238
249
  current = {
239
250
  id: msg.uuid ?? crypto.randomUUID(),
240
251
  userMessage: msg.message.content,
@@ -449,7 +460,7 @@ function buildTurns(messages) {
449
460
  continue;
450
461
  }
451
462
  }
452
- finalizeTurn();
463
+ finalizeTurn2();
453
464
  return turns;
454
465
  }
455
466
 
@@ -602,6 +613,315 @@ function computeStats(turns) {
602
613
  return stats;
603
614
  }
604
615
 
616
+ // src/lib/codex.ts
617
+ var SKIP_PROMPT_PREFIXES = [
618
+ "<environment_context>",
619
+ "<permissions instructions>",
620
+ "<collaboration_mode>",
621
+ "<skills_instructions>"
622
+ ];
623
+ function randomTurnId(prefix) {
624
+ return globalThis.crypto?.randomUUID?.() ?? `${prefix}-${Math.random().toString(36).slice(2, 10)}`;
625
+ }
626
+ function safeParseLine(line) {
627
+ try {
628
+ return JSON.parse(line);
629
+ } catch {
630
+ return null;
631
+ }
632
+ }
633
+ function isObject(value) {
634
+ return typeof value === "object" && value !== null;
635
+ }
636
+ function isCodexRecord(record) {
637
+ if (!record || typeof record.type !== "string") return false;
638
+ return record.type === "session_meta" || record.type === "turn_context" || record.type === "event_msg" || record.type === "response_item";
639
+ }
640
+ function extractMessageText(payload, blockType) {
641
+ const content = payload?.content;
642
+ if (!Array.isArray(content)) return "";
643
+ return content.filter((block) => isObject(block) && block.type === blockType && typeof block.text === "string").map((block) => block.text).join("\n").trim();
644
+ }
645
+ function normalizePromptText(text) {
646
+ const trimmed = text.trim();
647
+ if (!trimmed) return "";
648
+ if (SKIP_PROMPT_PREFIXES.some((prefix) => trimmed.startsWith(prefix))) return "";
649
+ return trimmed;
650
+ }
651
+ function mergeTokenUsage2(existing, incoming) {
652
+ if (!existing) return { ...incoming };
653
+ return {
654
+ input_tokens: existing.input_tokens + incoming.input_tokens,
655
+ output_tokens: existing.output_tokens + incoming.output_tokens,
656
+ cache_creation_input_tokens: (existing.cache_creation_input_tokens ?? 0) + (incoming.cache_creation_input_tokens ?? 0),
657
+ cache_read_input_tokens: (existing.cache_read_input_tokens ?? 0) + (incoming.cache_read_input_tokens ?? 0)
658
+ };
659
+ }
660
+ function parseTokenUsage(value) {
661
+ if (!isObject(value)) return null;
662
+ const inputTokens = typeof value.input_tokens === "number" ? value.input_tokens : 0;
663
+ const outputTokens = typeof value.output_tokens === "number" ? value.output_tokens : 0;
664
+ const cacheCreation = typeof value.cache_creation_input_tokens === "number" ? value.cache_creation_input_tokens : 0;
665
+ const cacheRead = typeof value.cache_read_input_tokens === "number" ? value.cache_read_input_tokens : 0;
666
+ if (inputTokens === 0 && outputTokens === 0 && cacheCreation === 0 && cacheRead === 0) return null;
667
+ return {
668
+ input_tokens: inputTokens,
669
+ output_tokens: outputTokens,
670
+ cache_creation_input_tokens: cacheCreation,
671
+ cache_read_input_tokens: cacheRead
672
+ };
673
+ }
674
+ function appendAssistantText(turn, text, timestamp) {
675
+ if (!text) return;
676
+ turn.assistantText.push(text);
677
+ const last = turn.contentBlocks[turn.contentBlocks.length - 1];
678
+ if (last && last.kind === "text") {
679
+ last.text.push(text);
680
+ return;
681
+ }
682
+ turn.contentBlocks.push({ kind: "text", text: [text], timestamp });
683
+ }
684
+ function appendThinking(turn, text, timestamp) {
685
+ if (!text) return;
686
+ const block = { type: "thinking", thinking: text, signature: "" };
687
+ turn.thinking.push(block);
688
+ const last = turn.contentBlocks[turn.contentBlocks.length - 1];
689
+ if (last && last.kind === "thinking") {
690
+ last.blocks.push(block);
691
+ return;
692
+ }
693
+ turn.contentBlocks.push({ kind: "thinking", blocks: [block], timestamp });
694
+ }
695
+ function appendToolCall(turn, toolCall, timestamp) {
696
+ turn.toolCalls.push(toolCall);
697
+ const last = turn.contentBlocks[turn.contentBlocks.length - 1];
698
+ if (last && last.kind === "tool_calls" && last.timestamp === timestamp) {
699
+ last.toolCalls.push(toolCall);
700
+ return;
701
+ }
702
+ turn.contentBlocks.push({ kind: "tool_calls", toolCalls: [toolCall], timestamp });
703
+ }
704
+ function finalizeTurn(turns, current, lastTimestamp) {
705
+ if (!current) return null;
706
+ const hasContent = current.userMessage !== null || current.assistantText.length > 0 || current.toolCalls.length > 0 || current.thinking.length > 0;
707
+ if (!hasContent) return null;
708
+ if (current.timestamp && lastTimestamp) {
709
+ const start = new Date(current.timestamp).getTime();
710
+ const end = new Date(lastTimestamp).getTime();
711
+ if (Number.isFinite(start) && Number.isFinite(end) && end >= start) {
712
+ current.durationMs = end - start;
713
+ }
714
+ }
715
+ turns.push(current);
716
+ return null;
717
+ }
718
+ function parseToolInput(argumentsText) {
719
+ if (typeof argumentsText !== "string" || !argumentsText.trim()) return {};
720
+ try {
721
+ const parsed = JSON.parse(argumentsText);
722
+ return isObject(parsed) ? parsed : { value: parsed };
723
+ } catch {
724
+ return { raw: argumentsText };
725
+ }
726
+ }
727
+ function inferToolError(output) {
728
+ if (!output) return false;
729
+ const exitMatch = output.match(/Process exited with code (\d+)/);
730
+ if (exitMatch) return exitMatch[1] !== "0";
731
+ return /\b(error|failed|exception)\b/i.test(output);
732
+ }
733
+ function createTurn(turnId, timestamp, model) {
734
+ return {
735
+ id: turnId || randomTurnId("codex-turn"),
736
+ userMessage: null,
737
+ contentBlocks: [],
738
+ thinking: [],
739
+ assistantText: [],
740
+ toolCalls: [],
741
+ subAgentActivity: [],
742
+ timestamp,
743
+ durationMs: null,
744
+ tokenUsage: null,
745
+ model
746
+ };
747
+ }
748
+ function extractPromptFromRecord(record) {
749
+ if (record.type === "event_msg" && isObject(record.payload) && record.payload.type === "user_message" && typeof record.payload.message === "string") {
750
+ return normalizePromptText(record.payload.message);
751
+ }
752
+ if (record.type === "response_item" && isObject(record.payload) && record.payload.type === "message" && record.payload.role === "user") {
753
+ return normalizePromptText(extractMessageText(record.payload, "input_text"));
754
+ }
755
+ return "";
756
+ }
757
+ function extractMetadataFromRecords(records) {
758
+ let sessionId = "";
759
+ let version = "";
760
+ let gitBranch = "";
761
+ let cwd = "";
762
+ let model = "";
763
+ let branchedFrom;
764
+ let firstUserMessage = "";
765
+ let lastUserMessage = "";
766
+ let timestamp = "";
767
+ let lastTimestamp = "";
768
+ let turnCount = 0;
769
+ let previousPrompt = "";
770
+ for (const record of records) {
771
+ if (!isObject(record.payload)) continue;
772
+ if (record.type === "session_meta") {
773
+ sessionId ||= typeof record.payload.id === "string" ? record.payload.id : "";
774
+ version ||= typeof record.payload.cli_version === "string" ? record.payload.cli_version : "";
775
+ cwd ||= typeof record.payload.cwd === "string" ? record.payload.cwd : "";
776
+ if (!branchedFrom && isObject(record.payload.branchedFrom)) {
777
+ const sourceId = typeof record.payload.branchedFrom.sessionId === "string" ? record.payload.branchedFrom.sessionId : "";
778
+ if (sourceId) {
779
+ branchedFrom = {
780
+ sessionId: sourceId,
781
+ turnIndex: typeof record.payload.branchedFrom.turnIndex === "number" ? record.payload.branchedFrom.turnIndex : null
782
+ };
783
+ }
784
+ }
785
+ const git = isObject(record.payload.git) ? record.payload.git : null;
786
+ gitBranch ||= git && typeof git.branch === "string" ? git.branch : "";
787
+ }
788
+ if (record.type === "turn_context") {
789
+ model ||= typeof record.payload.model === "string" ? record.payload.model : "";
790
+ cwd ||= typeof record.payload.cwd === "string" ? record.payload.cwd : "";
791
+ }
792
+ const prompt = extractPromptFromRecord(record);
793
+ if (!prompt) continue;
794
+ if (prompt === previousPrompt) continue;
795
+ if (!firstUserMessage) firstUserMessage = prompt;
796
+ lastUserMessage = prompt;
797
+ previousPrompt = prompt;
798
+ turnCount++;
799
+ if (!timestamp) timestamp = record.timestamp ?? "";
800
+ lastTimestamp = record.timestamp ?? lastTimestamp;
801
+ }
802
+ if (lastTimestamp === "") lastTimestamp = timestamp;
803
+ return {
804
+ sessionId,
805
+ version,
806
+ gitBranch,
807
+ cwd,
808
+ model,
809
+ slug: "",
810
+ branchedFrom,
811
+ firstUserMessage,
812
+ lastUserMessage,
813
+ timestamp,
814
+ lastTimestamp,
815
+ turnCount
816
+ };
817
+ }
818
+ function isCodexSessionText(jsonlText) {
819
+ for (const line of jsonlText.split("\n")) {
820
+ const trimmed = line.trim();
821
+ if (!trimmed) continue;
822
+ return isCodexRecord(safeParseLine(trimmed));
823
+ }
824
+ return false;
825
+ }
826
+ function extractCodexMetadataFromLines(lines) {
827
+ const records = lines.map(safeParseLine).filter(isCodexRecord);
828
+ return extractMetadataFromRecords(records);
829
+ }
830
+ function parseCodexSession(jsonlText) {
831
+ const records = jsonlText.split("\n").map((line) => line.trim()).filter(Boolean).map(safeParseLine).filter(isCodexRecord);
832
+ const metadata = extractMetadataFromRecords(records);
833
+ const turns = [];
834
+ const pendingToolCalls = /* @__PURE__ */ new Map();
835
+ let current = null;
836
+ let currentTurnId = null;
837
+ let currentModel = metadata.model || null;
838
+ let lastTurnTimestamp = "";
839
+ for (const record of records) {
840
+ const payload = isObject(record.payload) ? record.payload : void 0;
841
+ const timestamp = record.timestamp ?? "";
842
+ if (record.type === "turn_context") {
843
+ current = finalizeTurn(turns, current, lastTurnTimestamp);
844
+ currentTurnId = typeof payload?.turn_id === "string" ? payload.turn_id : null;
845
+ currentModel = typeof payload?.model === "string" ? payload.model : currentModel;
846
+ lastTurnTimestamp = timestamp;
847
+ continue;
848
+ }
849
+ if (record.type === "event_msg" && payload?.type === "user_message" && typeof payload.message === "string") {
850
+ if (current && (current.assistantText.length > 0 || current.toolCalls.length > 0 || current.thinking.length > 0)) {
851
+ current = finalizeTurn(turns, current, lastTurnTimestamp);
852
+ }
853
+ current ??= createTurn(currentTurnId, timestamp, currentModel);
854
+ current.userMessage = payload.message;
855
+ current.timestamp = current.timestamp || timestamp;
856
+ lastTurnTimestamp = timestamp;
857
+ continue;
858
+ }
859
+ current ??= createTurn(currentTurnId, timestamp, currentModel);
860
+ if (!current.model && currentModel) current.model = currentModel;
861
+ if (!current.timestamp) current.timestamp = timestamp;
862
+ if (timestamp) lastTurnTimestamp = timestamp;
863
+ if (record.type === "event_msg" && payload?.type === "token_count") {
864
+ const info = isObject(payload.info) ? payload.info : null;
865
+ const lastUsage = info ? parseTokenUsage(info.last_token_usage) : null;
866
+ if (lastUsage) {
867
+ current.tokenUsage = mergeTokenUsage2(current.tokenUsage, lastUsage);
868
+ }
869
+ continue;
870
+ }
871
+ if (record.type !== "response_item" || !payload) continue;
872
+ if (payload.type === "reasoning") {
873
+ const summary = Array.isArray(payload.summary) ? payload.summary.filter((item) => typeof item === "string").join("\n") : "";
874
+ appendThinking(current, summary.trim(), timestamp);
875
+ continue;
876
+ }
877
+ if (payload.type === "message" && payload.role === "assistant") {
878
+ appendAssistantText(current, extractMessageText(payload, "output_text"), timestamp);
879
+ continue;
880
+ }
881
+ if (payload.type === "message" && payload.role === "user" && current.userMessage === null) {
882
+ const text = normalizePromptText(extractMessageText(payload, "input_text"));
883
+ if (text) current.userMessage = text;
884
+ continue;
885
+ }
886
+ if (payload.type === "function_call" && typeof payload.call_id === "string") {
887
+ const toolCall = {
888
+ id: payload.call_id,
889
+ name: typeof payload.name === "string" ? payload.name : "tool",
890
+ input: parseToolInput(payload.arguments),
891
+ result: null,
892
+ isError: false,
893
+ timestamp
894
+ };
895
+ pendingToolCalls.set(toolCall.id, toolCall);
896
+ appendToolCall(current, toolCall, timestamp);
897
+ continue;
898
+ }
899
+ if (payload.type === "function_call_output" && typeof payload.call_id === "string") {
900
+ const toolCall = pendingToolCalls.get(payload.call_id);
901
+ if (!toolCall) continue;
902
+ const output = typeof payload.output === "string" ? payload.output : null;
903
+ toolCall.result = output;
904
+ toolCall.isError = inferToolError(output);
905
+ pendingToolCalls.delete(payload.call_id);
906
+ continue;
907
+ }
908
+ }
909
+ finalizeTurn(turns, current, lastTurnTimestamp);
910
+ return {
911
+ sessionId: metadata.sessionId,
912
+ version: metadata.version,
913
+ gitBranch: metadata.gitBranch,
914
+ cwd: metadata.cwd,
915
+ slug: metadata.slug,
916
+ model: metadata.model,
917
+ turns,
918
+ stats: computeStats(turns),
919
+ rawMessages: records,
920
+ branchedFrom: metadata.branchedFrom,
921
+ agentKind: "codex"
922
+ };
923
+ }
924
+
605
925
  // src/lib/parser.ts
606
926
  function isAssistantMessage2(msg) {
607
927
  return msg.type === "assistant";
@@ -622,6 +942,13 @@ function parseLines(jsonlText) {
622
942
  }
623
943
  return messages;
624
944
  }
945
+ function isCodexRawMessages(rawMessages) {
946
+ const firstType = rawMessages[0]?.type;
947
+ return firstType === "session_meta" || firstType === "turn_context" || firstType === "event_msg" || firstType === "response_item";
948
+ }
949
+ function serializeRawMessages(rawMessages) {
950
+ return rawMessages.map((msg) => JSON.stringify(msg)).join("\n");
951
+ }
625
952
  function extractSessionMetadata(messages) {
626
953
  const meta = { sessionId: "", version: "", gitBranch: "", cwd: "", slug: "", model: "", branchedFrom: void 0 };
627
954
  for (const msg of messages) {
@@ -640,19 +967,28 @@ function extractSessionMetadata(messages) {
640
967
  }
641
968
  return meta;
642
969
  }
643
- function parseSession(jsonlText) {
970
+ function parseSession(jsonlText, opts) {
971
+ if (isCodexSessionText(jsonlText)) {
972
+ return parseCodexSession(jsonlText);
973
+ }
644
974
  const rawMessages = parseLines(jsonlText);
645
975
  const metadata = extractSessionMetadata(rawMessages);
646
976
  const turns = buildTurns(rawMessages);
647
- const stats = computeStats(turns);
977
+ const stats = opts?.skipStats ? { totalInputTokens: 0, totalOutputTokens: 0, totalCacheCreationTokens: 0, totalCacheReadTokens: 0, totalCostUSD: 0, toolCallCounts: {}, errorCount: 0, totalDurationMs: 0, turnCount: turns.length } : computeStats(turns);
648
978
  return {
649
979
  ...metadata,
650
980
  turns,
651
981
  stats,
652
- rawMessages
982
+ rawMessages: opts?.skipStats ? [] : rawMessages,
983
+ agentKind: "claude"
653
984
  };
654
985
  }
655
986
  function parseSessionAppend(existing, newJsonlText) {
987
+ if (isCodexRawMessages(existing.rawMessages) || isCodexSessionText(newJsonlText)) {
988
+ const prefix = serializeRawMessages(existing.rawMessages);
989
+ return parseCodexSession(prefix ? `${prefix}
990
+ ${newJsonlText}` : newJsonlText);
991
+ }
656
992
  const newMessages = parseLines(newJsonlText);
657
993
  if (newMessages.length === 0) return existing;
658
994
  const allRawMessages = [...existing.rawMessages, ...newMessages];
@@ -736,6 +1072,10 @@ function getUserMessageImages(content) {
736
1072
  }
737
1073
 
738
1074
  // src/lib/search-index.ts
1075
+ var MAX_CONTENT_LEN = 4096;
1076
+ function truncContent(text) {
1077
+ return text.length > MAX_CONTENT_LEN ? text.slice(0, MAX_CONTENT_LEN) : text;
1078
+ }
739
1079
  var SearchIndex = class {
740
1080
  db;
741
1081
  dbPath;
@@ -769,7 +1109,7 @@ var SearchIndex = class {
769
1109
  source_file,
770
1110
  location,
771
1111
  content,
772
- tokenize = 'trigram'
1112
+ tokenize = 'unicode61'
773
1113
  )`);
774
1114
  }
775
1115
  }
@@ -796,6 +1136,60 @@ var SearchIndex = class {
796
1136
  lastUpdate: this._lastUpdate
797
1137
  };
798
1138
  }
1139
+ /**
1140
+ * Insert all searchable content from a parsed session into the FTS5 index.
1141
+ * Shared by both `indexFile` (single-file) and `buildFull` (batch).
1142
+ */
1143
+ insertSessionContent(insert, sessionId, filePath, session) {
1144
+ for (let i = 0; i < session.turns.length; i++) {
1145
+ const turn = session.turns[i];
1146
+ const prefix = `turn/${i}`;
1147
+ const userText = getUserMessageText(turn.userMessage);
1148
+ if (userText.trim()) {
1149
+ insert.run(sessionId, filePath, `${prefix}/userMessage`, truncContent(userText));
1150
+ }
1151
+ const assistantJoined = turn.assistantText.join("\n\n").trim();
1152
+ if (assistantJoined) {
1153
+ insert.run(sessionId, filePath, `${prefix}/assistantMessage`, truncContent(assistantJoined));
1154
+ }
1155
+ const thinkingText = turn.thinking.filter((t) => t.thinking && t.thinking.length > 0).map((t) => t.thinking).join("\n\n").trim();
1156
+ if (thinkingText) {
1157
+ insert.run(sessionId, filePath, `${prefix}/thinking`, truncContent(thinkingText));
1158
+ }
1159
+ for (const tc of turn.toolCalls) {
1160
+ const inputStr = JSON.stringify(tc.input);
1161
+ if (inputStr && inputStr !== "{}") {
1162
+ insert.run(sessionId, filePath, `${prefix}/toolCall/${tc.id}/input`, truncContent(inputStr));
1163
+ }
1164
+ if (tc.result) {
1165
+ insert.run(sessionId, filePath, `${prefix}/toolCall/${tc.id}/result`, truncContent(tc.result));
1166
+ }
1167
+ }
1168
+ for (const sa of turn.subAgentActivity) {
1169
+ const saPrefix = `agent/${sa.agentId}`;
1170
+ const saText = sa.text.join("\n\n").trim();
1171
+ if (saText) {
1172
+ insert.run(sessionId, filePath, `${saPrefix}/assistantMessage`, truncContent(saText));
1173
+ }
1174
+ const saThinking = sa.thinking.filter((t) => t.length > 0).join("\n\n").trim();
1175
+ if (saThinking) {
1176
+ insert.run(sessionId, filePath, `${saPrefix}/thinking`, truncContent(saThinking));
1177
+ }
1178
+ for (const tc of sa.toolCalls) {
1179
+ const inputStr = JSON.stringify(tc.input);
1180
+ if (inputStr && inputStr !== "{}") {
1181
+ insert.run(sessionId, filePath, `${saPrefix}/toolCall/${tc.id}/input`, truncContent(inputStr));
1182
+ }
1183
+ if (tc.result) {
1184
+ insert.run(sessionId, filePath, `${saPrefix}/toolCall/${tc.id}/result`, truncContent(tc.result));
1185
+ }
1186
+ }
1187
+ }
1188
+ if (turn.compactionSummary) {
1189
+ insert.run(sessionId, filePath, `${prefix}/compactionSummary`, truncContent(turn.compactionSummary));
1190
+ }
1191
+ }
1192
+ }
799
1193
  /**
800
1194
  * Parse a JSONL file and insert all searchable content into the FTS5 index.
801
1195
  * Idempotent: deletes old data for the file before re-indexing.
@@ -809,7 +1203,7 @@ var SearchIndex = class {
809
1203
  mtimeMs = (0, import_node_fs.statSync)(filePath).mtimeMs;
810
1204
  }
811
1205
  const content = (0, import_node_fs.readFileSync)(filePath, "utf-8");
812
- const session = parseSession(content);
1206
+ const session = parseSession(content, { skipStats: true });
813
1207
  const isSubagent = opts?.isSubagent ? 1 : 0;
814
1208
  const parentSessionId = opts?.parentSessionId ?? null;
815
1209
  const insert = this.db.prepare(
@@ -827,54 +1221,7 @@ var SearchIndex = class {
827
1221
  const txn = this.db.transaction(() => {
828
1222
  deleteContent.run(filePath);
829
1223
  deleteFile.run(filePath);
830
- for (let i = 0; i < session.turns.length; i++) {
831
- const turn = session.turns[i];
832
- const prefix = `turn/${i}`;
833
- const userText = getUserMessageText(turn.userMessage);
834
- if (userText.trim()) {
835
- insert.run(sessionId, filePath, `${prefix}/userMessage`, userText);
836
- }
837
- const assistantJoined = turn.assistantText.join("\n\n").trim();
838
- if (assistantJoined) {
839
- insert.run(sessionId, filePath, `${prefix}/assistantMessage`, assistantJoined);
840
- }
841
- const thinkingText = turn.thinking.filter((t) => t.thinking && t.thinking.length > 0).map((t) => t.thinking).join("\n\n").trim();
842
- if (thinkingText) {
843
- insert.run(sessionId, filePath, `${prefix}/thinking`, thinkingText);
844
- }
845
- for (const tc of turn.toolCalls) {
846
- const inputStr = JSON.stringify(tc.input);
847
- if (inputStr && inputStr !== "{}") {
848
- insert.run(sessionId, filePath, `${prefix}/toolCall/${tc.id}/input`, inputStr);
849
- }
850
- if (tc.result) {
851
- insert.run(sessionId, filePath, `${prefix}/toolCall/${tc.id}/result`, tc.result);
852
- }
853
- }
854
- for (const sa of turn.subAgentActivity) {
855
- const saPrefix = `agent/${sa.agentId}`;
856
- const saText = sa.text.join("\n\n").trim();
857
- if (saText) {
858
- insert.run(sessionId, filePath, `${saPrefix}/assistantMessage`, saText);
859
- }
860
- const saThinking = sa.thinking.filter((t) => t.length > 0).join("\n\n").trim();
861
- if (saThinking) {
862
- insert.run(sessionId, filePath, `${saPrefix}/thinking`, saThinking);
863
- }
864
- for (const tc of sa.toolCalls) {
865
- const inputStr = JSON.stringify(tc.input);
866
- if (inputStr && inputStr !== "{}") {
867
- insert.run(sessionId, filePath, `${saPrefix}/toolCall/${tc.id}/input`, inputStr);
868
- }
869
- if (tc.result) {
870
- insert.run(sessionId, filePath, `${saPrefix}/toolCall/${tc.id}/result`, tc.result);
871
- }
872
- }
873
- }
874
- if (turn.compactionSummary) {
875
- insert.run(sessionId, filePath, `${prefix}/compactionSummary`, turn.compactionSummary);
876
- }
877
- }
1224
+ this.insertSessionContent(insert, sessionId, filePath, session);
878
1225
  insertFile.run(filePath, mtimeMs, sessionId, isSubagent, parentSessionId);
879
1226
  });
880
1227
  txn();
@@ -925,7 +1272,7 @@ var SearchIndex = class {
925
1272
  location: row.location,
926
1273
  snippet: row.snippet,
927
1274
  matchCount: 1
928
- // FTS5 trigram doesn't expose per-row match count; 1 = "at least one match"
1275
+ // FTS5 doesn't expose per-row match count; 1 = "at least one match"
929
1276
  }));
930
1277
  if (caseSensitive) {
931
1278
  hits = hits.filter((h) => h.snippet.includes(query));
@@ -964,19 +1311,67 @@ var SearchIndex = class {
964
1311
  * Structure: projectsDir/{projectName}/{sessionId}.jsonl
965
1312
  * Subagents: projectsDir/{projectName}/{sessionId}/subagents/agent-{id}.jsonl
966
1313
  *
1314
+ * Optimized: discovers all files first, then processes them in a single
1315
+ * SQLite transaction with pre-prepared statements. This avoids the overhead
1316
+ * of 3000+ individual transactions (each forcing a disk sync).
1317
+ *
967
1318
  * Stores `projectsDir` as a class field so `rebuild()` can reuse it.
968
1319
  */
969
1320
  buildFull(projectsDir) {
970
1321
  this.projectsDir = projectsDir;
971
- this.db.exec("DELETE FROM search_content");
972
- this.db.exec("DELETE FROM indexed_files");
973
- this.indexProjectsDir(projectsDir);
1322
+ this.db.close();
1323
+ try {
1324
+ (0, import_node_fs.unlinkSync)(this.dbPath);
1325
+ } catch {
1326
+ }
1327
+ try {
1328
+ (0, import_node_fs.unlinkSync)(this.dbPath + "-wal");
1329
+ } catch {
1330
+ }
1331
+ try {
1332
+ (0, import_node_fs.unlinkSync)(this.dbPath + "-shm");
1333
+ } catch {
1334
+ }
1335
+ this.db = new Database(this.dbPath);
1336
+ this.db.exec("PRAGMA journal_mode = WAL");
1337
+ this.db.exec("PRAGMA synchronous = OFF");
1338
+ this.db.exec("PRAGMA cache_size = -64000");
1339
+ this.db.exec("PRAGMA temp_store = MEMORY");
1340
+ this.db.exec("PRAGMA mmap_size = 268435456");
1341
+ this.initSchema();
1342
+ const files = [];
1343
+ this.discoverFiles(projectsDir, (filePath, sessionId, mtimeMs, isSubagent, parentSessionId) => {
1344
+ files.push({ path: filePath, sessionId, mtimeMs, isSubagent, parentSessionId });
1345
+ });
1346
+ const insert = this.db.prepare(
1347
+ "INSERT INTO search_content (session_id, source_file, location, content) VALUES (?, ?, ?, ?)"
1348
+ );
1349
+ const insertFile = this.db.prepare(
1350
+ "INSERT OR REPLACE INTO indexed_files (file_path, mtime_ms, session_id, is_subagent, parent_session_id) VALUES (?, ?, ?, ?, ?)"
1351
+ );
1352
+ const txn = this.db.transaction(() => {
1353
+ for (const file of files) {
1354
+ try {
1355
+ const content = (0, import_node_fs.readFileSync)(file.path, "utf-8");
1356
+ const session = parseSession(content, { skipStats: true });
1357
+ this.insertSessionContent(insert, file.sessionId, file.path, session);
1358
+ insertFile.run(file.path, file.mtimeMs, file.sessionId, file.isSubagent ? 1 : 0, file.parentSessionId);
1359
+ } catch {
1360
+ }
1361
+ }
1362
+ });
1363
+ txn();
1364
+ this.db.exec("PRAGMA synchronous = NORMAL");
974
1365
  this._lastFullBuild = (/* @__PURE__ */ new Date()).toISOString();
975
1366
  this._lastUpdate = (/* @__PURE__ */ new Date()).toISOString();
976
1367
  }
977
1368
  /**
978
1369
  * Incrementally re-index only files whose mtime has changed since last index.
979
1370
  * New files (not in indexed_files) are always indexed.
1371
+ *
1372
+ * WARNING: This walks ALL files under projectsDir and stats each one.
1373
+ * On large session stores (3000+ files) this can take minutes.
1374
+ * Prefer `updateRecent()` for CLI search paths.
980
1375
  */
981
1376
  updateStale(projectsDir) {
982
1377
  this.projectsDir = projectsDir;
@@ -1003,6 +1398,45 @@ var SearchIndex = class {
1003
1398
  this._lastUpdate = (/* @__PURE__ */ new Date()).toISOString();
1004
1399
  }
1005
1400
  }
1401
+ /**
1402
+ * Lightweight incremental update for CLI search paths.
1403
+ *
1404
+ * Instead of stat-ing every file, only discovers files modified after the
1405
+ * most recent mtime in the index. Caps re-indexing to `maxFiles` to prevent
1406
+ * blocking on large backlogs (run `index rebuild` for a full catch-up).
1407
+ */
1408
+ updateRecent(projectsDir, maxFiles = 50) {
1409
+ this.projectsDir = projectsDir;
1410
+ const row = this.db.prepare(
1411
+ "SELECT MAX(mtime_ms) as max_mtime FROM indexed_files"
1412
+ ).get();
1413
+ const highWater = row?.max_mtime ?? 0;
1414
+ const getIndexed = this.db.prepare(
1415
+ "SELECT mtime_ms FROM indexed_files WHERE file_path = ?"
1416
+ );
1417
+ const filesToIndex = [];
1418
+ this.discoverFiles(projectsDir, (filePath, sessionId, mtimeMs, isSubagent, parentSessionId) => {
1419
+ if (mtimeMs <= highWater) {
1420
+ const existing = getIndexed.get(filePath);
1421
+ if (existing && existing.mtime_ms >= mtimeMs) return;
1422
+ }
1423
+ filesToIndex.push({ path: filePath, sessionId, mtimeMs, isSubagent, parentSessionId });
1424
+ });
1425
+ filesToIndex.sort((a, b) => b.mtimeMs - a.mtimeMs);
1426
+ const batch = filesToIndex.slice(0, maxFiles);
1427
+ for (const file of batch) {
1428
+ try {
1429
+ this.indexFile(file.path, file.sessionId, file.mtimeMs, {
1430
+ isSubagent: file.isSubagent,
1431
+ parentSessionId: file.parentSessionId
1432
+ });
1433
+ } catch {
1434
+ }
1435
+ }
1436
+ if (batch.length > 0) {
1437
+ this._lastUpdate = (/* @__PURE__ */ new Date()).toISOString();
1438
+ }
1439
+ }
1006
1440
  /**
1007
1441
  * Re-run buildFull using the previously stored projectsDir.
1008
1442
  * No-op if projectsDir was never set.
@@ -1011,18 +1445,6 @@ var SearchIndex = class {
1011
1445
  if (!this.projectsDir) return;
1012
1446
  this.buildFull(this.projectsDir);
1013
1447
  }
1014
- /**
1015
- * Walk all project directories under `projectsDir` and index every discovered
1016
- * JSONL file (both sessions and subagents).
1017
- */
1018
- indexProjectsDir(projectsDir) {
1019
- this.discoverFiles(projectsDir, (filePath, sessionId, mtimeMs, isSubagent, parentSessionId) => {
1020
- try {
1021
- this.indexFile(filePath, sessionId, mtimeMs, { isSubagent, parentSessionId });
1022
- } catch {
1023
- }
1024
- });
1025
- }
1026
1448
  /**
1027
1449
  * Walk the projects directory tree and invoke `callback` for every JSONL file.
1028
1450
  *
@@ -1038,29 +1460,23 @@ var SearchIndex = class {
1038
1460
  discoverFiles(projectsDir, callback) {
1039
1461
  let entries;
1040
1462
  try {
1041
- entries = (0, import_node_fs.readdirSync)(projectsDir);
1463
+ entries = (0, import_node_fs.readdirSync)(projectsDir, { withFileTypes: true });
1042
1464
  } catch {
1043
1465
  return;
1044
1466
  }
1045
- for (const projectName of entries) {
1046
- if (projectName === "memory") continue;
1047
- const projectDir = (0, import_node_path.join)(projectsDir, projectName);
1048
- try {
1049
- const s = (0, import_node_fs.statSync)(projectDir);
1050
- if (!s.isDirectory()) continue;
1051
- } catch {
1052
- continue;
1053
- }
1467
+ for (const entry of entries) {
1468
+ if (entry.name === "memory" || !entry.isDirectory()) continue;
1469
+ const projectDir = (0, import_node_path.join)(projectsDir, entry.name);
1054
1470
  let files;
1055
1471
  try {
1056
- files = (0, import_node_fs.readdirSync)(projectDir);
1472
+ files = (0, import_node_fs.readdirSync)(projectDir, { withFileTypes: true });
1057
1473
  } catch {
1058
1474
  continue;
1059
1475
  }
1060
1476
  for (const file of files) {
1061
- if (!file.endsWith(".jsonl")) continue;
1062
- const filePath = (0, import_node_path.join)(projectDir, file);
1063
- const sessionId = (0, import_node_path.basename)(file, ".jsonl");
1477
+ if (!file.name.endsWith(".jsonl") || !file.isFile()) continue;
1478
+ const filePath = (0, import_node_path.join)(projectDir, file.name);
1479
+ const sessionId = (0, import_node_path.basename)(file.name, ".jsonl");
1064
1480
  try {
1065
1481
  const s = (0, import_node_fs.statSync)(filePath);
1066
1482
  callback(filePath, sessionId, s.mtimeMs, false, null);
@@ -1234,6 +1650,29 @@ async function findJsonlPath(sessionId) {
1234
1650
  }
1235
1651
  } catch {
1236
1652
  }
1653
+ const codexRoot = (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".codex", "sessions");
1654
+ const walk = async (dir, depth) => {
1655
+ if (depth > 4) return null;
1656
+ let entries;
1657
+ try {
1658
+ entries = await (0, import_promises.readdir)(dir, { withFileTypes: true });
1659
+ } catch {
1660
+ return null;
1661
+ }
1662
+ for (const entry of entries) {
1663
+ const filePath = (0, import_node_path3.join)(dir, entry.name);
1664
+ if (entry.isDirectory()) {
1665
+ const match = await walk(filePath, depth + 1);
1666
+ if (match) return match;
1667
+ continue;
1668
+ }
1669
+ if (!entry.name.endsWith(".jsonl")) continue;
1670
+ if (entry.name.endsWith(`${sessionId}.jsonl`)) return filePath;
1671
+ }
1672
+ return null;
1673
+ };
1674
+ const codexMatch = await walk(codexRoot, 0);
1675
+ if (codexMatch) return codexMatch;
1237
1676
  return null;
1238
1677
  }
1239
1678
  async function matchSubagentToMember(leadSessionId, subagentFileName, members) {
@@ -1288,10 +1727,19 @@ async function searchSessions(query, opts, searchIndex) {
1288
1727
  const depth = Math.min(Math.max(1, opts.depth ?? 4), 4);
1289
1728
  let index = searchIndex ?? null;
1290
1729
  let ownedIndex = false;
1291
- if (!index && (0, import_node_fs2.existsSync)(DEFAULT_DB_PATH)) {
1730
+ if (!index && searchIndex === void 0) {
1292
1731
  try {
1732
+ const dbExists = (0, import_node_fs2.existsSync)(DEFAULT_DB_PATH);
1733
+ if (!dbExists) {
1734
+ (0, import_node_fs2.mkdirSync)((0, import_node_path4.dirname)(DEFAULT_DB_PATH), { recursive: true });
1735
+ }
1293
1736
  index = new SearchIndex(DEFAULT_DB_PATH);
1294
1737
  ownedIndex = true;
1738
+ if (!dbExists) {
1739
+ index.buildFull(dirs.PROJECTS_DIR);
1740
+ } else {
1741
+ index.updateRecent(dirs.PROJECTS_DIR);
1742
+ }
1295
1743
  } catch {
1296
1744
  }
1297
1745
  }
@@ -1365,6 +1813,14 @@ async function cwdFromFilePath(filePath) {
1365
1813
  cwdCache.set(filePath, obj.cwd);
1366
1814
  return obj.cwd;
1367
1815
  }
1816
+ if (obj.type === "session_meta" && obj.payload?.cwd) {
1817
+ cwdCache.set(filePath, obj.payload.cwd);
1818
+ return obj.payload.cwd;
1819
+ }
1820
+ if (obj.type === "turn_context" && obj.payload?.cwd) {
1821
+ cwdCache.set(filePath, obj.payload.cwd);
1822
+ return obj.payload.cwd;
1823
+ }
1368
1824
  } catch {
1369
1825
  }
1370
1826
  }
@@ -1873,12 +2329,17 @@ async function getAgentTurnDetail(sessionId, agentId, turnIndex) {
1873
2329
  // src/commands/sessions.ts
1874
2330
  var import_promises5 = require("node:fs/promises");
1875
2331
  var import_node_path6 = require("node:path");
2332
+ var import_node_os3 = require("node:os");
1876
2333
 
1877
2334
  // src/lib/metadata.ts
1878
2335
  var import_promises4 = require("node:fs/promises");
1879
2336
 
1880
2337
  // src/lib/sessionStatus.ts
1881
2338
  function deriveSessionStatus(rawMessages) {
2339
+ const firstType = rawMessages[0]?.type;
2340
+ if (firstType === "session_meta" || firstType === "turn_context" || firstType === "event_msg" || firstType === "response_item") {
2341
+ return deriveCodexSessionStatus(rawMessages);
2342
+ }
1882
2343
  let pendingEnqueues = 0;
1883
2344
  function result(status, toolName) {
1884
2345
  const info = { status, pendingQueue: Math.max(0, pendingEnqueues) };
@@ -1919,7 +2380,38 @@ function deriveSessionStatus(rawMessages) {
1919
2380
  if (isMeta) continue;
1920
2381
  return result("processing");
1921
2382
  }
1922
- if (msg.type === "summary") return result("compacting");
2383
+ if (msg.type === "summary") continue;
2384
+ if (msg.type === "system" && msg.subtype === "compact_boundary") continue;
2385
+ }
2386
+ return { status: "idle" };
2387
+ }
2388
+ function deriveCodexSessionStatus(rawMessages) {
2389
+ for (let i = rawMessages.length - 1; i >= 0; i--) {
2390
+ const msg = rawMessages[i];
2391
+ if (msg.type === "event_msg") {
2392
+ const payload = msg.payload;
2393
+ switch (payload?.type) {
2394
+ case "task_complete":
2395
+ return { status: "completed" };
2396
+ case "task_started":
2397
+ return { status: "processing" };
2398
+ case "agent_message":
2399
+ return { status: "thinking" };
2400
+ case "token_count":
2401
+ continue;
2402
+ }
2403
+ }
2404
+ if (msg.type === "response_item") {
2405
+ const payload = msg.payload;
2406
+ if (!payload) continue;
2407
+ if (payload.type === "function_call") {
2408
+ return { status: "tool_use", toolName: payload.name };
2409
+ }
2410
+ if (payload.type === "message") {
2411
+ if (payload.role === "assistant") return { status: "thinking" };
2412
+ if (payload.role === "user") return { status: "processing" };
2413
+ }
2414
+ }
1923
2415
  }
1924
2416
  return { status: "idle" };
1925
2417
  }
@@ -1945,6 +2437,36 @@ async function getSessionMeta(filePath) {
1945
2437
  const content = await (0, import_promises4.readFile)(filePath, "utf-8");
1946
2438
  lines = content.split("\n").filter(Boolean);
1947
2439
  }
2440
+ let firstParsed = null;
2441
+ if (lines.length > 0) {
2442
+ try {
2443
+ firstParsed = JSON.parse(lines[0]);
2444
+ } catch {
2445
+ firstParsed = null;
2446
+ }
2447
+ }
2448
+ const isCodex = firstParsed?.type === "session_meta" || firstParsed?.type === "turn_context";
2449
+ if (isCodex) {
2450
+ if (isPartialRead) {
2451
+ const content = await (0, import_promises4.readFile)(filePath, "utf-8");
2452
+ lines = content.split("\n").filter(Boolean);
2453
+ }
2454
+ const meta = extractCodexMetadataFromLines(lines);
2455
+ return {
2456
+ sessionId: meta.sessionId,
2457
+ version: meta.version,
2458
+ gitBranch: meta.gitBranch,
2459
+ model: meta.model,
2460
+ slug: meta.slug,
2461
+ cwd: meta.cwd,
2462
+ firstUserMessage: meta.firstUserMessage,
2463
+ lastUserMessage: meta.lastUserMessage,
2464
+ timestamp: meta.timestamp,
2465
+ turnCount: meta.turnCount,
2466
+ lineCount: lines.length,
2467
+ branchedFrom: meta.branchedFrom
2468
+ };
2469
+ }
1948
2470
  let sessionId = "";
1949
2471
  let version = "";
1950
2472
  let gitBranch = "";
@@ -2041,6 +2563,30 @@ async function getSessionStatus(filePath) {
2041
2563
  } catch {
2042
2564
  continue;
2043
2565
  }
2566
+ if (obj.type === "event_msg") {
2567
+ const payload = obj.payload;
2568
+ switch (payload?.type) {
2569
+ case "task_complete":
2570
+ return { status: "completed" };
2571
+ case "task_started":
2572
+ return { status: "processing" };
2573
+ case "agent_message":
2574
+ return { status: "thinking" };
2575
+ case "token_count":
2576
+ continue;
2577
+ }
2578
+ }
2579
+ if (obj.type === "response_item") {
2580
+ const payload = obj.payload;
2581
+ if (payload?.type === "function_call") {
2582
+ return { status: "tool_use", toolName: payload.name };
2583
+ }
2584
+ if (payload?.type === "message") {
2585
+ const role = payload.role;
2586
+ if (role === "assistant") return { status: "thinking" };
2587
+ if (role === "user") return { status: "processing" };
2588
+ }
2589
+ }
2044
2590
  if (obj.type === "assistant" || obj.type === "user" || obj.type === "queue-operation") {
2045
2591
  meaningful.unshift(obj);
2046
2592
  const isEndTurn = obj.type === "assistant" && obj.message?.stop_reason === "end_turn";
@@ -2059,6 +2605,35 @@ async function getSessionStatus(filePath) {
2059
2605
  }
2060
2606
 
2061
2607
  // src/commands/sessions.ts
2608
+ var CODEX_SESSIONS_DIR = (0, import_node_path6.join)((0, import_node_os3.homedir)(), ".codex", "sessions");
2609
+ async function listCodexSessionFiles(cutoff) {
2610
+ const walk = async (dir, depth) => {
2611
+ if (depth > 4) return [];
2612
+ let entries;
2613
+ try {
2614
+ entries = await (0, import_promises5.readdir)(dir, { withFileTypes: true });
2615
+ } catch {
2616
+ return [];
2617
+ }
2618
+ const results = [];
2619
+ for (const entry of entries) {
2620
+ const filePath = (0, import_node_path6.join)(dir, entry.name);
2621
+ if (entry.isDirectory()) {
2622
+ results.push(...await walk(filePath, depth + 1));
2623
+ continue;
2624
+ }
2625
+ if (!entry.name.endsWith(".jsonl")) continue;
2626
+ try {
2627
+ const s = await (0, import_promises5.stat)(filePath);
2628
+ if (s.mtimeMs >= cutoff) results.push({ path: filePath, mtimeMs: s.mtimeMs });
2629
+ } catch {
2630
+ continue;
2631
+ }
2632
+ }
2633
+ return results;
2634
+ };
2635
+ return walk(CODEX_SESSIONS_DIR, 0);
2636
+ }
2062
2637
  async function listSessions(opts = {}) {
2063
2638
  const limit = Math.min(Math.max(1, opts.limit ?? 20), 100);
2064
2639
  const maxAgeMs = parseMaxAge(opts.maxAge ?? "7d");
@@ -2092,7 +2667,8 @@ async function listSessions(opts = {}) {
2092
2667
  }
2093
2668
  })
2094
2669
  );
2095
- const allFiles = nested.flat();
2670
+ const codexFiles = await listCodexSessionFiles(cutoff);
2671
+ const allFiles = [...nested.flat(), ...codexFiles];
2096
2672
  allFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
2097
2673
  const results = [];
2098
2674
  for (const file of allFiles) {
@@ -2115,7 +2691,8 @@ async function listSessions(opts = {}) {
2115
2691
  lastMessage: meta.lastUserMessage,
2116
2692
  turnCount: meta.turnCount,
2117
2693
  status: statusInfo.status,
2118
- mtime: file.mtimeMs
2694
+ mtime: file.mtimeMs,
2695
+ source: file.path.startsWith(CODEX_SESSIONS_DIR + "/") ? "codex" : "claude"
2119
2696
  });
2120
2697
  } catch {
2121
2698
  }
@@ -2129,10 +2706,9 @@ async function currentSession(cwd) {
2129
2706
  try {
2130
2707
  files = await (0, import_promises5.readdir)(projectDir);
2131
2708
  } catch {
2132
- return null;
2709
+ files = [];
2133
2710
  }
2134
2711
  const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
2135
- if (jsonlFiles.length === 0) return null;
2136
2712
  const statResults = await Promise.all(
2137
2713
  jsonlFiles.map(async (f) => {
2138
2714
  const filePath = (0, import_node_path6.join)(projectDir, f);
@@ -2144,7 +2720,21 @@ async function currentSession(cwd) {
2144
2720
  }
2145
2721
  })
2146
2722
  );
2147
- const valid = statResults.filter((r) => r !== null);
2723
+ const codexCandidates = await listCodexSessionFiles(0);
2724
+ const codexMatches = [];
2725
+ for (const file of codexCandidates) {
2726
+ try {
2727
+ const meta2 = await getSessionMeta(file.path);
2728
+ if (meta2.cwd === cwd) codexMatches.push(file);
2729
+ } catch {
2730
+ continue;
2731
+ }
2732
+ }
2733
+ const valid = [
2734
+ ...statResults.filter((r) => r !== null),
2735
+ ...codexMatches
2736
+ ];
2737
+ if (valid.length === 0) return null;
2148
2738
  valid.sort((a, b) => b.mtimeMs - a.mtimeMs);
2149
2739
  const latest = valid[0];
2150
2740
  if (!latest) return null;
@@ -2164,7 +2754,8 @@ async function currentSession(cwd) {
2164
2754
  lastMessage: meta.lastUserMessage,
2165
2755
  turnCount: meta.turnCount,
2166
2756
  status: statusInfo.status,
2167
- mtime: latest.mtimeMs
2757
+ mtime: latest.mtimeMs,
2758
+ source: latest.path.startsWith(CODEX_SESSIONS_DIR + "/") ? "codex" : "claude"
2168
2759
  };
2169
2760
  }
2170
2761