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/cli.js CHANGED
@@ -64,6 +64,9 @@ function isSystemMessage(msg) {
64
64
  function isSummaryMessage(msg) {
65
65
  return msg.type === "summary";
66
66
  }
67
+ function isCompactBoundary(msg) {
68
+ return msg.type === "system" && msg.subtype === "compact_boundary";
69
+ }
67
70
  function extractTextFromContent(content) {
68
71
  if (typeof content === "string") return content;
69
72
  return content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
@@ -143,7 +146,7 @@ function buildTurns(messages) {
143
146
  agentBlockMap.set(parentId, block);
144
147
  }
145
148
  }
146
- function finalizeTurn() {
149
+ function finalizeTurn2() {
147
150
  if (!current) return;
148
151
  for (const tc of current.toolCalls) {
149
152
  flushSubAgentMessages(tc.id);
@@ -157,13 +160,21 @@ function buildTurns(messages) {
157
160
  }
158
161
  for (const msg of messages) {
159
162
  if (isSummaryMessage(msg)) {
160
- finalizeTurn();
163
+ finalizeTurn2();
161
164
  pendingCompaction = buildCompactionSummary(
162
165
  turns,
163
166
  msg.summary ?? "Conversation compacted"
164
167
  );
165
168
  continue;
166
169
  }
170
+ if (isCompactBoundary(msg)) {
171
+ finalizeTurn2();
172
+ pendingCompaction = buildCompactionSummary(
173
+ turns,
174
+ msg.content ?? "Conversation compacted"
175
+ );
176
+ continue;
177
+ }
167
178
  if (isUserMessage(msg) && !msg.isMeta) {
168
179
  const content = msg.message.content;
169
180
  if (typeof content !== "string" && Array.isArray(content)) {
@@ -225,7 +236,7 @@ function buildTurns(messages) {
225
236
  continue;
226
237
  }
227
238
  }
228
- finalizeTurn();
239
+ finalizeTurn2();
229
240
  current = {
230
241
  id: msg.uuid ?? crypto.randomUUID(),
231
242
  userMessage: msg.message.content,
@@ -440,7 +451,7 @@ function buildTurns(messages) {
440
451
  continue;
441
452
  }
442
453
  }
443
- finalizeTurn();
454
+ finalizeTurn2();
444
455
  return turns;
445
456
  }
446
457
 
@@ -593,6 +604,315 @@ function computeStats(turns) {
593
604
  return stats;
594
605
  }
595
606
 
607
+ // src/lib/codex.ts
608
+ var SKIP_PROMPT_PREFIXES = [
609
+ "<environment_context>",
610
+ "<permissions instructions>",
611
+ "<collaboration_mode>",
612
+ "<skills_instructions>"
613
+ ];
614
+ function randomTurnId(prefix) {
615
+ return globalThis.crypto?.randomUUID?.() ?? `${prefix}-${Math.random().toString(36).slice(2, 10)}`;
616
+ }
617
+ function safeParseLine(line) {
618
+ try {
619
+ return JSON.parse(line);
620
+ } catch {
621
+ return null;
622
+ }
623
+ }
624
+ function isObject(value) {
625
+ return typeof value === "object" && value !== null;
626
+ }
627
+ function isCodexRecord(record) {
628
+ if (!record || typeof record.type !== "string") return false;
629
+ return record.type === "session_meta" || record.type === "turn_context" || record.type === "event_msg" || record.type === "response_item";
630
+ }
631
+ function extractMessageText(payload, blockType) {
632
+ const content = payload?.content;
633
+ if (!Array.isArray(content)) return "";
634
+ return content.filter((block) => isObject(block) && block.type === blockType && typeof block.text === "string").map((block) => block.text).join("\n").trim();
635
+ }
636
+ function normalizePromptText(text) {
637
+ const trimmed = text.trim();
638
+ if (!trimmed) return "";
639
+ if (SKIP_PROMPT_PREFIXES.some((prefix) => trimmed.startsWith(prefix))) return "";
640
+ return trimmed;
641
+ }
642
+ function mergeTokenUsage2(existing, incoming) {
643
+ if (!existing) return { ...incoming };
644
+ return {
645
+ input_tokens: existing.input_tokens + incoming.input_tokens,
646
+ output_tokens: existing.output_tokens + incoming.output_tokens,
647
+ cache_creation_input_tokens: (existing.cache_creation_input_tokens ?? 0) + (incoming.cache_creation_input_tokens ?? 0),
648
+ cache_read_input_tokens: (existing.cache_read_input_tokens ?? 0) + (incoming.cache_read_input_tokens ?? 0)
649
+ };
650
+ }
651
+ function parseTokenUsage(value) {
652
+ if (!isObject(value)) return null;
653
+ const inputTokens = typeof value.input_tokens === "number" ? value.input_tokens : 0;
654
+ const outputTokens = typeof value.output_tokens === "number" ? value.output_tokens : 0;
655
+ const cacheCreation = typeof value.cache_creation_input_tokens === "number" ? value.cache_creation_input_tokens : 0;
656
+ const cacheRead = typeof value.cache_read_input_tokens === "number" ? value.cache_read_input_tokens : 0;
657
+ if (inputTokens === 0 && outputTokens === 0 && cacheCreation === 0 && cacheRead === 0) return null;
658
+ return {
659
+ input_tokens: inputTokens,
660
+ output_tokens: outputTokens,
661
+ cache_creation_input_tokens: cacheCreation,
662
+ cache_read_input_tokens: cacheRead
663
+ };
664
+ }
665
+ function appendAssistantText(turn, text, timestamp) {
666
+ if (!text) return;
667
+ turn.assistantText.push(text);
668
+ const last = turn.contentBlocks[turn.contentBlocks.length - 1];
669
+ if (last && last.kind === "text") {
670
+ last.text.push(text);
671
+ return;
672
+ }
673
+ turn.contentBlocks.push({ kind: "text", text: [text], timestamp });
674
+ }
675
+ function appendThinking(turn, text, timestamp) {
676
+ if (!text) return;
677
+ const block = { type: "thinking", thinking: text, signature: "" };
678
+ turn.thinking.push(block);
679
+ const last = turn.contentBlocks[turn.contentBlocks.length - 1];
680
+ if (last && last.kind === "thinking") {
681
+ last.blocks.push(block);
682
+ return;
683
+ }
684
+ turn.contentBlocks.push({ kind: "thinking", blocks: [block], timestamp });
685
+ }
686
+ function appendToolCall(turn, toolCall, timestamp) {
687
+ turn.toolCalls.push(toolCall);
688
+ const last = turn.contentBlocks[turn.contentBlocks.length - 1];
689
+ if (last && last.kind === "tool_calls" && last.timestamp === timestamp) {
690
+ last.toolCalls.push(toolCall);
691
+ return;
692
+ }
693
+ turn.contentBlocks.push({ kind: "tool_calls", toolCalls: [toolCall], timestamp });
694
+ }
695
+ function finalizeTurn(turns, current, lastTimestamp) {
696
+ if (!current) return null;
697
+ const hasContent = current.userMessage !== null || current.assistantText.length > 0 || current.toolCalls.length > 0 || current.thinking.length > 0;
698
+ if (!hasContent) return null;
699
+ if (current.timestamp && lastTimestamp) {
700
+ const start = new Date(current.timestamp).getTime();
701
+ const end = new Date(lastTimestamp).getTime();
702
+ if (Number.isFinite(start) && Number.isFinite(end) && end >= start) {
703
+ current.durationMs = end - start;
704
+ }
705
+ }
706
+ turns.push(current);
707
+ return null;
708
+ }
709
+ function parseToolInput(argumentsText) {
710
+ if (typeof argumentsText !== "string" || !argumentsText.trim()) return {};
711
+ try {
712
+ const parsed = JSON.parse(argumentsText);
713
+ return isObject(parsed) ? parsed : { value: parsed };
714
+ } catch {
715
+ return { raw: argumentsText };
716
+ }
717
+ }
718
+ function inferToolError(output) {
719
+ if (!output) return false;
720
+ const exitMatch = output.match(/Process exited with code (\d+)/);
721
+ if (exitMatch) return exitMatch[1] !== "0";
722
+ return /\b(error|failed|exception)\b/i.test(output);
723
+ }
724
+ function createTurn(turnId, timestamp, model) {
725
+ return {
726
+ id: turnId || randomTurnId("codex-turn"),
727
+ userMessage: null,
728
+ contentBlocks: [],
729
+ thinking: [],
730
+ assistantText: [],
731
+ toolCalls: [],
732
+ subAgentActivity: [],
733
+ timestamp,
734
+ durationMs: null,
735
+ tokenUsage: null,
736
+ model
737
+ };
738
+ }
739
+ function extractPromptFromRecord(record) {
740
+ if (record.type === "event_msg" && isObject(record.payload) && record.payload.type === "user_message" && typeof record.payload.message === "string") {
741
+ return normalizePromptText(record.payload.message);
742
+ }
743
+ if (record.type === "response_item" && isObject(record.payload) && record.payload.type === "message" && record.payload.role === "user") {
744
+ return normalizePromptText(extractMessageText(record.payload, "input_text"));
745
+ }
746
+ return "";
747
+ }
748
+ function extractMetadataFromRecords(records) {
749
+ let sessionId = "";
750
+ let version = "";
751
+ let gitBranch = "";
752
+ let cwd = "";
753
+ let model = "";
754
+ let branchedFrom;
755
+ let firstUserMessage = "";
756
+ let lastUserMessage = "";
757
+ let timestamp = "";
758
+ let lastTimestamp = "";
759
+ let turnCount = 0;
760
+ let previousPrompt = "";
761
+ for (const record of records) {
762
+ if (!isObject(record.payload)) continue;
763
+ if (record.type === "session_meta") {
764
+ sessionId ||= typeof record.payload.id === "string" ? record.payload.id : "";
765
+ version ||= typeof record.payload.cli_version === "string" ? record.payload.cli_version : "";
766
+ cwd ||= typeof record.payload.cwd === "string" ? record.payload.cwd : "";
767
+ if (!branchedFrom && isObject(record.payload.branchedFrom)) {
768
+ const sourceId = typeof record.payload.branchedFrom.sessionId === "string" ? record.payload.branchedFrom.sessionId : "";
769
+ if (sourceId) {
770
+ branchedFrom = {
771
+ sessionId: sourceId,
772
+ turnIndex: typeof record.payload.branchedFrom.turnIndex === "number" ? record.payload.branchedFrom.turnIndex : null
773
+ };
774
+ }
775
+ }
776
+ const git = isObject(record.payload.git) ? record.payload.git : null;
777
+ gitBranch ||= git && typeof git.branch === "string" ? git.branch : "";
778
+ }
779
+ if (record.type === "turn_context") {
780
+ model ||= typeof record.payload.model === "string" ? record.payload.model : "";
781
+ cwd ||= typeof record.payload.cwd === "string" ? record.payload.cwd : "";
782
+ }
783
+ const prompt = extractPromptFromRecord(record);
784
+ if (!prompt) continue;
785
+ if (prompt === previousPrompt) continue;
786
+ if (!firstUserMessage) firstUserMessage = prompt;
787
+ lastUserMessage = prompt;
788
+ previousPrompt = prompt;
789
+ turnCount++;
790
+ if (!timestamp) timestamp = record.timestamp ?? "";
791
+ lastTimestamp = record.timestamp ?? lastTimestamp;
792
+ }
793
+ if (lastTimestamp === "") lastTimestamp = timestamp;
794
+ return {
795
+ sessionId,
796
+ version,
797
+ gitBranch,
798
+ cwd,
799
+ model,
800
+ slug: "",
801
+ branchedFrom,
802
+ firstUserMessage,
803
+ lastUserMessage,
804
+ timestamp,
805
+ lastTimestamp,
806
+ turnCount
807
+ };
808
+ }
809
+ function isCodexSessionText(jsonlText) {
810
+ for (const line of jsonlText.split("\n")) {
811
+ const trimmed = line.trim();
812
+ if (!trimmed) continue;
813
+ return isCodexRecord(safeParseLine(trimmed));
814
+ }
815
+ return false;
816
+ }
817
+ function extractCodexMetadataFromLines(lines) {
818
+ const records = lines.map(safeParseLine).filter(isCodexRecord);
819
+ return extractMetadataFromRecords(records);
820
+ }
821
+ function parseCodexSession(jsonlText) {
822
+ const records = jsonlText.split("\n").map((line) => line.trim()).filter(Boolean).map(safeParseLine).filter(isCodexRecord);
823
+ const metadata = extractMetadataFromRecords(records);
824
+ const turns = [];
825
+ const pendingToolCalls = /* @__PURE__ */ new Map();
826
+ let current = null;
827
+ let currentTurnId = null;
828
+ let currentModel = metadata.model || null;
829
+ let lastTurnTimestamp = "";
830
+ for (const record of records) {
831
+ const payload = isObject(record.payload) ? record.payload : void 0;
832
+ const timestamp = record.timestamp ?? "";
833
+ if (record.type === "turn_context") {
834
+ current = finalizeTurn(turns, current, lastTurnTimestamp);
835
+ currentTurnId = typeof payload?.turn_id === "string" ? payload.turn_id : null;
836
+ currentModel = typeof payload?.model === "string" ? payload.model : currentModel;
837
+ lastTurnTimestamp = timestamp;
838
+ continue;
839
+ }
840
+ if (record.type === "event_msg" && payload?.type === "user_message" && typeof payload.message === "string") {
841
+ if (current && (current.assistantText.length > 0 || current.toolCalls.length > 0 || current.thinking.length > 0)) {
842
+ current = finalizeTurn(turns, current, lastTurnTimestamp);
843
+ }
844
+ current ??= createTurn(currentTurnId, timestamp, currentModel);
845
+ current.userMessage = payload.message;
846
+ current.timestamp = current.timestamp || timestamp;
847
+ lastTurnTimestamp = timestamp;
848
+ continue;
849
+ }
850
+ current ??= createTurn(currentTurnId, timestamp, currentModel);
851
+ if (!current.model && currentModel) current.model = currentModel;
852
+ if (!current.timestamp) current.timestamp = timestamp;
853
+ if (timestamp) lastTurnTimestamp = timestamp;
854
+ if (record.type === "event_msg" && payload?.type === "token_count") {
855
+ const info = isObject(payload.info) ? payload.info : null;
856
+ const lastUsage = info ? parseTokenUsage(info.last_token_usage) : null;
857
+ if (lastUsage) {
858
+ current.tokenUsage = mergeTokenUsage2(current.tokenUsage, lastUsage);
859
+ }
860
+ continue;
861
+ }
862
+ if (record.type !== "response_item" || !payload) continue;
863
+ if (payload.type === "reasoning") {
864
+ const summary = Array.isArray(payload.summary) ? payload.summary.filter((item) => typeof item === "string").join("\n") : "";
865
+ appendThinking(current, summary.trim(), timestamp);
866
+ continue;
867
+ }
868
+ if (payload.type === "message" && payload.role === "assistant") {
869
+ appendAssistantText(current, extractMessageText(payload, "output_text"), timestamp);
870
+ continue;
871
+ }
872
+ if (payload.type === "message" && payload.role === "user" && current.userMessage === null) {
873
+ const text = normalizePromptText(extractMessageText(payload, "input_text"));
874
+ if (text) current.userMessage = text;
875
+ continue;
876
+ }
877
+ if (payload.type === "function_call" && typeof payload.call_id === "string") {
878
+ const toolCall = {
879
+ id: payload.call_id,
880
+ name: typeof payload.name === "string" ? payload.name : "tool",
881
+ input: parseToolInput(payload.arguments),
882
+ result: null,
883
+ isError: false,
884
+ timestamp
885
+ };
886
+ pendingToolCalls.set(toolCall.id, toolCall);
887
+ appendToolCall(current, toolCall, timestamp);
888
+ continue;
889
+ }
890
+ if (payload.type === "function_call_output" && typeof payload.call_id === "string") {
891
+ const toolCall = pendingToolCalls.get(payload.call_id);
892
+ if (!toolCall) continue;
893
+ const output = typeof payload.output === "string" ? payload.output : null;
894
+ toolCall.result = output;
895
+ toolCall.isError = inferToolError(output);
896
+ pendingToolCalls.delete(payload.call_id);
897
+ continue;
898
+ }
899
+ }
900
+ finalizeTurn(turns, current, lastTurnTimestamp);
901
+ return {
902
+ sessionId: metadata.sessionId,
903
+ version: metadata.version,
904
+ gitBranch: metadata.gitBranch,
905
+ cwd: metadata.cwd,
906
+ slug: metadata.slug,
907
+ model: metadata.model,
908
+ turns,
909
+ stats: computeStats(turns),
910
+ rawMessages: records,
911
+ branchedFrom: metadata.branchedFrom,
912
+ agentKind: "codex"
913
+ };
914
+ }
915
+
596
916
  // src/lib/parser.ts
597
917
  function isAssistantMessage2(msg) {
598
918
  return msg.type === "assistant";
@@ -631,16 +951,20 @@ function extractSessionMetadata(messages) {
631
951
  }
632
952
  return meta;
633
953
  }
634
- function parseSession(jsonlText) {
954
+ function parseSession(jsonlText, opts) {
955
+ if (isCodexSessionText(jsonlText)) {
956
+ return parseCodexSession(jsonlText);
957
+ }
635
958
  const rawMessages = parseLines(jsonlText);
636
959
  const metadata = extractSessionMetadata(rawMessages);
637
960
  const turns = buildTurns(rawMessages);
638
- const stats = computeStats(turns);
961
+ const stats = opts?.skipStats ? { totalInputTokens: 0, totalOutputTokens: 0, totalCacheCreationTokens: 0, totalCacheReadTokens: 0, totalCostUSD: 0, toolCallCounts: {}, errorCount: 0, totalDurationMs: 0, turnCount: turns.length } : computeStats(turns);
639
962
  return {
640
963
  ...metadata,
641
964
  turns,
642
965
  stats,
643
- rawMessages
966
+ rawMessages: opts?.skipStats ? [] : rawMessages,
967
+ agentKind: "claude"
644
968
  };
645
969
  }
646
970
  function getUserMessageText(content) {
@@ -650,6 +974,10 @@ function getUserMessageText(content) {
650
974
  }
651
975
 
652
976
  // src/lib/search-index.ts
977
+ var MAX_CONTENT_LEN = 4096;
978
+ function truncContent(text) {
979
+ return text.length > MAX_CONTENT_LEN ? text.slice(0, MAX_CONTENT_LEN) : text;
980
+ }
653
981
  var SearchIndex = class {
654
982
  db;
655
983
  dbPath;
@@ -683,7 +1011,7 @@ var SearchIndex = class {
683
1011
  source_file,
684
1012
  location,
685
1013
  content,
686
- tokenize = 'trigram'
1014
+ tokenize = 'unicode61'
687
1015
  )`);
688
1016
  }
689
1017
  }
@@ -710,6 +1038,60 @@ var SearchIndex = class {
710
1038
  lastUpdate: this._lastUpdate
711
1039
  };
712
1040
  }
1041
+ /**
1042
+ * Insert all searchable content from a parsed session into the FTS5 index.
1043
+ * Shared by both `indexFile` (single-file) and `buildFull` (batch).
1044
+ */
1045
+ insertSessionContent(insert, sessionId, filePath, session) {
1046
+ for (let i = 0; i < session.turns.length; i++) {
1047
+ const turn = session.turns[i];
1048
+ const prefix = `turn/${i}`;
1049
+ const userText = getUserMessageText(turn.userMessage);
1050
+ if (userText.trim()) {
1051
+ insert.run(sessionId, filePath, `${prefix}/userMessage`, truncContent(userText));
1052
+ }
1053
+ const assistantJoined = turn.assistantText.join("\n\n").trim();
1054
+ if (assistantJoined) {
1055
+ insert.run(sessionId, filePath, `${prefix}/assistantMessage`, truncContent(assistantJoined));
1056
+ }
1057
+ const thinkingText = turn.thinking.filter((t) => t.thinking && t.thinking.length > 0).map((t) => t.thinking).join("\n\n").trim();
1058
+ if (thinkingText) {
1059
+ insert.run(sessionId, filePath, `${prefix}/thinking`, truncContent(thinkingText));
1060
+ }
1061
+ for (const tc of turn.toolCalls) {
1062
+ const inputStr = JSON.stringify(tc.input);
1063
+ if (inputStr && inputStr !== "{}") {
1064
+ insert.run(sessionId, filePath, `${prefix}/toolCall/${tc.id}/input`, truncContent(inputStr));
1065
+ }
1066
+ if (tc.result) {
1067
+ insert.run(sessionId, filePath, `${prefix}/toolCall/${tc.id}/result`, truncContent(tc.result));
1068
+ }
1069
+ }
1070
+ for (const sa of turn.subAgentActivity) {
1071
+ const saPrefix = `agent/${sa.agentId}`;
1072
+ const saText = sa.text.join("\n\n").trim();
1073
+ if (saText) {
1074
+ insert.run(sessionId, filePath, `${saPrefix}/assistantMessage`, truncContent(saText));
1075
+ }
1076
+ const saThinking = sa.thinking.filter((t) => t.length > 0).join("\n\n").trim();
1077
+ if (saThinking) {
1078
+ insert.run(sessionId, filePath, `${saPrefix}/thinking`, truncContent(saThinking));
1079
+ }
1080
+ for (const tc of sa.toolCalls) {
1081
+ const inputStr = JSON.stringify(tc.input);
1082
+ if (inputStr && inputStr !== "{}") {
1083
+ insert.run(sessionId, filePath, `${saPrefix}/toolCall/${tc.id}/input`, truncContent(inputStr));
1084
+ }
1085
+ if (tc.result) {
1086
+ insert.run(sessionId, filePath, `${saPrefix}/toolCall/${tc.id}/result`, truncContent(tc.result));
1087
+ }
1088
+ }
1089
+ }
1090
+ if (turn.compactionSummary) {
1091
+ insert.run(sessionId, filePath, `${prefix}/compactionSummary`, truncContent(turn.compactionSummary));
1092
+ }
1093
+ }
1094
+ }
713
1095
  /**
714
1096
  * Parse a JSONL file and insert all searchable content into the FTS5 index.
715
1097
  * Idempotent: deletes old data for the file before re-indexing.
@@ -723,7 +1105,7 @@ var SearchIndex = class {
723
1105
  mtimeMs = (0, import_node_fs.statSync)(filePath).mtimeMs;
724
1106
  }
725
1107
  const content = (0, import_node_fs.readFileSync)(filePath, "utf-8");
726
- const session = parseSession(content);
1108
+ const session = parseSession(content, { skipStats: true });
727
1109
  const isSubagent = opts?.isSubagent ? 1 : 0;
728
1110
  const parentSessionId = opts?.parentSessionId ?? null;
729
1111
  const insert = this.db.prepare(
@@ -741,54 +1123,7 @@ var SearchIndex = class {
741
1123
  const txn = this.db.transaction(() => {
742
1124
  deleteContent.run(filePath);
743
1125
  deleteFile.run(filePath);
744
- for (let i = 0; i < session.turns.length; i++) {
745
- const turn = session.turns[i];
746
- const prefix = `turn/${i}`;
747
- const userText = getUserMessageText(turn.userMessage);
748
- if (userText.trim()) {
749
- insert.run(sessionId, filePath, `${prefix}/userMessage`, userText);
750
- }
751
- const assistantJoined = turn.assistantText.join("\n\n").trim();
752
- if (assistantJoined) {
753
- insert.run(sessionId, filePath, `${prefix}/assistantMessage`, assistantJoined);
754
- }
755
- const thinkingText = turn.thinking.filter((t) => t.thinking && t.thinking.length > 0).map((t) => t.thinking).join("\n\n").trim();
756
- if (thinkingText) {
757
- insert.run(sessionId, filePath, `${prefix}/thinking`, thinkingText);
758
- }
759
- for (const tc of turn.toolCalls) {
760
- const inputStr = JSON.stringify(tc.input);
761
- if (inputStr && inputStr !== "{}") {
762
- insert.run(sessionId, filePath, `${prefix}/toolCall/${tc.id}/input`, inputStr);
763
- }
764
- if (tc.result) {
765
- insert.run(sessionId, filePath, `${prefix}/toolCall/${tc.id}/result`, tc.result);
766
- }
767
- }
768
- for (const sa of turn.subAgentActivity) {
769
- const saPrefix = `agent/${sa.agentId}`;
770
- const saText = sa.text.join("\n\n").trim();
771
- if (saText) {
772
- insert.run(sessionId, filePath, `${saPrefix}/assistantMessage`, saText);
773
- }
774
- const saThinking = sa.thinking.filter((t) => t.length > 0).join("\n\n").trim();
775
- if (saThinking) {
776
- insert.run(sessionId, filePath, `${saPrefix}/thinking`, saThinking);
777
- }
778
- for (const tc of sa.toolCalls) {
779
- const inputStr = JSON.stringify(tc.input);
780
- if (inputStr && inputStr !== "{}") {
781
- insert.run(sessionId, filePath, `${saPrefix}/toolCall/${tc.id}/input`, inputStr);
782
- }
783
- if (tc.result) {
784
- insert.run(sessionId, filePath, `${saPrefix}/toolCall/${tc.id}/result`, tc.result);
785
- }
786
- }
787
- }
788
- if (turn.compactionSummary) {
789
- insert.run(sessionId, filePath, `${prefix}/compactionSummary`, turn.compactionSummary);
790
- }
791
- }
1126
+ this.insertSessionContent(insert, sessionId, filePath, session);
792
1127
  insertFile.run(filePath, mtimeMs, sessionId, isSubagent, parentSessionId);
793
1128
  });
794
1129
  txn();
@@ -839,7 +1174,7 @@ var SearchIndex = class {
839
1174
  location: row.location,
840
1175
  snippet: row.snippet,
841
1176
  matchCount: 1
842
- // FTS5 trigram doesn't expose per-row match count; 1 = "at least one match"
1177
+ // FTS5 doesn't expose per-row match count; 1 = "at least one match"
843
1178
  }));
844
1179
  if (caseSensitive) {
845
1180
  hits = hits.filter((h) => h.snippet.includes(query));
@@ -878,19 +1213,67 @@ var SearchIndex = class {
878
1213
  * Structure: projectsDir/{projectName}/{sessionId}.jsonl
879
1214
  * Subagents: projectsDir/{projectName}/{sessionId}/subagents/agent-{id}.jsonl
880
1215
  *
1216
+ * Optimized: discovers all files first, then processes them in a single
1217
+ * SQLite transaction with pre-prepared statements. This avoids the overhead
1218
+ * of 3000+ individual transactions (each forcing a disk sync).
1219
+ *
881
1220
  * Stores `projectsDir` as a class field so `rebuild()` can reuse it.
882
1221
  */
883
1222
  buildFull(projectsDir) {
884
1223
  this.projectsDir = projectsDir;
885
- this.db.exec("DELETE FROM search_content");
886
- this.db.exec("DELETE FROM indexed_files");
887
- this.indexProjectsDir(projectsDir);
1224
+ this.db.close();
1225
+ try {
1226
+ (0, import_node_fs.unlinkSync)(this.dbPath);
1227
+ } catch {
1228
+ }
1229
+ try {
1230
+ (0, import_node_fs.unlinkSync)(this.dbPath + "-wal");
1231
+ } catch {
1232
+ }
1233
+ try {
1234
+ (0, import_node_fs.unlinkSync)(this.dbPath + "-shm");
1235
+ } catch {
1236
+ }
1237
+ this.db = new Database(this.dbPath);
1238
+ this.db.exec("PRAGMA journal_mode = WAL");
1239
+ this.db.exec("PRAGMA synchronous = OFF");
1240
+ this.db.exec("PRAGMA cache_size = -64000");
1241
+ this.db.exec("PRAGMA temp_store = MEMORY");
1242
+ this.db.exec("PRAGMA mmap_size = 268435456");
1243
+ this.initSchema();
1244
+ const files = [];
1245
+ this.discoverFiles(projectsDir, (filePath, sessionId, mtimeMs, isSubagent, parentSessionId) => {
1246
+ files.push({ path: filePath, sessionId, mtimeMs, isSubagent, parentSessionId });
1247
+ });
1248
+ const insert = this.db.prepare(
1249
+ "INSERT INTO search_content (session_id, source_file, location, content) VALUES (?, ?, ?, ?)"
1250
+ );
1251
+ const insertFile = this.db.prepare(
1252
+ "INSERT OR REPLACE INTO indexed_files (file_path, mtime_ms, session_id, is_subagent, parent_session_id) VALUES (?, ?, ?, ?, ?)"
1253
+ );
1254
+ const txn = this.db.transaction(() => {
1255
+ for (const file of files) {
1256
+ try {
1257
+ const content = (0, import_node_fs.readFileSync)(file.path, "utf-8");
1258
+ const session = parseSession(content, { skipStats: true });
1259
+ this.insertSessionContent(insert, file.sessionId, file.path, session);
1260
+ insertFile.run(file.path, file.mtimeMs, file.sessionId, file.isSubagent ? 1 : 0, file.parentSessionId);
1261
+ } catch {
1262
+ }
1263
+ }
1264
+ });
1265
+ txn();
1266
+ this.db.exec("PRAGMA synchronous = NORMAL");
888
1267
  this._lastFullBuild = (/* @__PURE__ */ new Date()).toISOString();
889
1268
  this._lastUpdate = (/* @__PURE__ */ new Date()).toISOString();
890
1269
  }
891
1270
  /**
892
1271
  * Incrementally re-index only files whose mtime has changed since last index.
893
1272
  * New files (not in indexed_files) are always indexed.
1273
+ *
1274
+ * WARNING: This walks ALL files under projectsDir and stats each one.
1275
+ * On large session stores (3000+ files) this can take minutes.
1276
+ * Prefer `updateRecent()` for CLI search paths.
894
1277
  */
895
1278
  updateStale(projectsDir) {
896
1279
  this.projectsDir = projectsDir;
@@ -917,6 +1300,45 @@ var SearchIndex = class {
917
1300
  this._lastUpdate = (/* @__PURE__ */ new Date()).toISOString();
918
1301
  }
919
1302
  }
1303
+ /**
1304
+ * Lightweight incremental update for CLI search paths.
1305
+ *
1306
+ * Instead of stat-ing every file, only discovers files modified after the
1307
+ * most recent mtime in the index. Caps re-indexing to `maxFiles` to prevent
1308
+ * blocking on large backlogs (run `index rebuild` for a full catch-up).
1309
+ */
1310
+ updateRecent(projectsDir, maxFiles = 50) {
1311
+ this.projectsDir = projectsDir;
1312
+ const row = this.db.prepare(
1313
+ "SELECT MAX(mtime_ms) as max_mtime FROM indexed_files"
1314
+ ).get();
1315
+ const highWater = row?.max_mtime ?? 0;
1316
+ const getIndexed = this.db.prepare(
1317
+ "SELECT mtime_ms FROM indexed_files WHERE file_path = ?"
1318
+ );
1319
+ const filesToIndex = [];
1320
+ this.discoverFiles(projectsDir, (filePath, sessionId, mtimeMs, isSubagent, parentSessionId) => {
1321
+ if (mtimeMs <= highWater) {
1322
+ const existing = getIndexed.get(filePath);
1323
+ if (existing && existing.mtime_ms >= mtimeMs) return;
1324
+ }
1325
+ filesToIndex.push({ path: filePath, sessionId, mtimeMs, isSubagent, parentSessionId });
1326
+ });
1327
+ filesToIndex.sort((a, b) => b.mtimeMs - a.mtimeMs);
1328
+ const batch = filesToIndex.slice(0, maxFiles);
1329
+ for (const file of batch) {
1330
+ try {
1331
+ this.indexFile(file.path, file.sessionId, file.mtimeMs, {
1332
+ isSubagent: file.isSubagent,
1333
+ parentSessionId: file.parentSessionId
1334
+ });
1335
+ } catch {
1336
+ }
1337
+ }
1338
+ if (batch.length > 0) {
1339
+ this._lastUpdate = (/* @__PURE__ */ new Date()).toISOString();
1340
+ }
1341
+ }
920
1342
  /**
921
1343
  * Re-run buildFull using the previously stored projectsDir.
922
1344
  * No-op if projectsDir was never set.
@@ -925,18 +1347,6 @@ var SearchIndex = class {
925
1347
  if (!this.projectsDir) return;
926
1348
  this.buildFull(this.projectsDir);
927
1349
  }
928
- /**
929
- * Walk all project directories under `projectsDir` and index every discovered
930
- * JSONL file (both sessions and subagents).
931
- */
932
- indexProjectsDir(projectsDir) {
933
- this.discoverFiles(projectsDir, (filePath, sessionId, mtimeMs, isSubagent, parentSessionId) => {
934
- try {
935
- this.indexFile(filePath, sessionId, mtimeMs, { isSubagent, parentSessionId });
936
- } catch {
937
- }
938
- });
939
- }
940
1350
  /**
941
1351
  * Walk the projects directory tree and invoke `callback` for every JSONL file.
942
1352
  *
@@ -952,29 +1362,23 @@ var SearchIndex = class {
952
1362
  discoverFiles(projectsDir, callback) {
953
1363
  let entries;
954
1364
  try {
955
- entries = (0, import_node_fs.readdirSync)(projectsDir);
1365
+ entries = (0, import_node_fs.readdirSync)(projectsDir, { withFileTypes: true });
956
1366
  } catch {
957
1367
  return;
958
1368
  }
959
- for (const projectName of entries) {
960
- if (projectName === "memory") continue;
961
- const projectDir = (0, import_node_path.join)(projectsDir, projectName);
962
- try {
963
- const s = (0, import_node_fs.statSync)(projectDir);
964
- if (!s.isDirectory()) continue;
965
- } catch {
966
- continue;
967
- }
1369
+ for (const entry of entries) {
1370
+ if (entry.name === "memory" || !entry.isDirectory()) continue;
1371
+ const projectDir = (0, import_node_path.join)(projectsDir, entry.name);
968
1372
  let files;
969
1373
  try {
970
- files = (0, import_node_fs.readdirSync)(projectDir);
1374
+ files = (0, import_node_fs.readdirSync)(projectDir, { withFileTypes: true });
971
1375
  } catch {
972
1376
  continue;
973
1377
  }
974
1378
  for (const file of files) {
975
- if (!file.endsWith(".jsonl")) continue;
976
- const filePath = (0, import_node_path.join)(projectDir, file);
977
- const sessionId = (0, import_node_path.basename)(file, ".jsonl");
1379
+ if (!file.name.endsWith(".jsonl") || !file.isFile()) continue;
1380
+ const filePath = (0, import_node_path.join)(projectDir, file.name);
1381
+ const sessionId = (0, import_node_path.basename)(file.name, ".jsonl");
978
1382
  try {
979
1383
  const s = (0, import_node_fs.statSync)(filePath);
980
1384
  callback(filePath, sessionId, s.mtimeMs, false, null);
@@ -1143,6 +1547,29 @@ async function findJsonlPath(sessionId) {
1143
1547
  }
1144
1548
  } catch {
1145
1549
  }
1550
+ const codexRoot = (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".codex", "sessions");
1551
+ const walk = async (dir, depth) => {
1552
+ if (depth > 4) return null;
1553
+ let entries;
1554
+ try {
1555
+ entries = await (0, import_promises.readdir)(dir, { withFileTypes: true });
1556
+ } catch {
1557
+ return null;
1558
+ }
1559
+ for (const entry of entries) {
1560
+ const filePath = (0, import_node_path3.join)(dir, entry.name);
1561
+ if (entry.isDirectory()) {
1562
+ const match = await walk(filePath, depth + 1);
1563
+ if (match) return match;
1564
+ continue;
1565
+ }
1566
+ if (!entry.name.endsWith(".jsonl")) continue;
1567
+ if (entry.name.endsWith(`${sessionId}.jsonl`)) return filePath;
1568
+ }
1569
+ return null;
1570
+ };
1571
+ const codexMatch = await walk(codexRoot, 0);
1572
+ if (codexMatch) return codexMatch;
1146
1573
  return null;
1147
1574
  }
1148
1575
  async function matchSubagentToMember(leadSessionId, subagentFileName, members) {
@@ -1197,10 +1624,19 @@ async function searchSessions(query, opts, searchIndex) {
1197
1624
  const depth = Math.min(Math.max(1, opts.depth ?? 4), 4);
1198
1625
  let index = searchIndex ?? null;
1199
1626
  let ownedIndex = false;
1200
- if (!index && (0, import_node_fs2.existsSync)(DEFAULT_DB_PATH)) {
1627
+ if (!index && searchIndex === void 0) {
1201
1628
  try {
1629
+ const dbExists = (0, import_node_fs2.existsSync)(DEFAULT_DB_PATH);
1630
+ if (!dbExists) {
1631
+ (0, import_node_fs2.mkdirSync)((0, import_node_path4.dirname)(DEFAULT_DB_PATH), { recursive: true });
1632
+ }
1202
1633
  index = new SearchIndex(DEFAULT_DB_PATH);
1203
1634
  ownedIndex = true;
1635
+ if (!dbExists) {
1636
+ index.buildFull(dirs.PROJECTS_DIR);
1637
+ } else {
1638
+ index.updateRecent(dirs.PROJECTS_DIR);
1639
+ }
1204
1640
  } catch {
1205
1641
  }
1206
1642
  }
@@ -1274,6 +1710,14 @@ async function cwdFromFilePath(filePath) {
1274
1710
  cwdCache.set(filePath, obj.cwd);
1275
1711
  return obj.cwd;
1276
1712
  }
1713
+ if (obj.type === "session_meta" && obj.payload?.cwd) {
1714
+ cwdCache.set(filePath, obj.payload.cwd);
1715
+ return obj.payload.cwd;
1716
+ }
1717
+ if (obj.type === "turn_context" && obj.payload?.cwd) {
1718
+ cwdCache.set(filePath, obj.payload.cwd);
1719
+ return obj.payload.cwd;
1720
+ }
1277
1721
  } catch {
1278
1722
  }
1279
1723
  }
@@ -1782,12 +2226,17 @@ async function getAgentTurnDetail(sessionId, agentId, turnIndex) {
1782
2226
  // src/commands/sessions.ts
1783
2227
  var import_promises5 = require("node:fs/promises");
1784
2228
  var import_node_path6 = require("node:path");
2229
+ var import_node_os3 = require("node:os");
1785
2230
 
1786
2231
  // src/lib/metadata.ts
1787
2232
  var import_promises4 = require("node:fs/promises");
1788
2233
 
1789
2234
  // src/lib/sessionStatus.ts
1790
2235
  function deriveSessionStatus(rawMessages) {
2236
+ const firstType = rawMessages[0]?.type;
2237
+ if (firstType === "session_meta" || firstType === "turn_context" || firstType === "event_msg" || firstType === "response_item") {
2238
+ return deriveCodexSessionStatus(rawMessages);
2239
+ }
1791
2240
  let pendingEnqueues = 0;
1792
2241
  function result(status, toolName) {
1793
2242
  const info = { status, pendingQueue: Math.max(0, pendingEnqueues) };
@@ -1828,7 +2277,38 @@ function deriveSessionStatus(rawMessages) {
1828
2277
  if (isMeta) continue;
1829
2278
  return result("processing");
1830
2279
  }
1831
- if (msg.type === "summary") return result("compacting");
2280
+ if (msg.type === "summary") continue;
2281
+ if (msg.type === "system" && msg.subtype === "compact_boundary") continue;
2282
+ }
2283
+ return { status: "idle" };
2284
+ }
2285
+ function deriveCodexSessionStatus(rawMessages) {
2286
+ for (let i = rawMessages.length - 1; i >= 0; i--) {
2287
+ const msg = rawMessages[i];
2288
+ if (msg.type === "event_msg") {
2289
+ const payload = msg.payload;
2290
+ switch (payload?.type) {
2291
+ case "task_complete":
2292
+ return { status: "completed" };
2293
+ case "task_started":
2294
+ return { status: "processing" };
2295
+ case "agent_message":
2296
+ return { status: "thinking" };
2297
+ case "token_count":
2298
+ continue;
2299
+ }
2300
+ }
2301
+ if (msg.type === "response_item") {
2302
+ const payload = msg.payload;
2303
+ if (!payload) continue;
2304
+ if (payload.type === "function_call") {
2305
+ return { status: "tool_use", toolName: payload.name };
2306
+ }
2307
+ if (payload.type === "message") {
2308
+ if (payload.role === "assistant") return { status: "thinking" };
2309
+ if (payload.role === "user") return { status: "processing" };
2310
+ }
2311
+ }
1832
2312
  }
1833
2313
  return { status: "idle" };
1834
2314
  }
@@ -1854,6 +2334,36 @@ async function getSessionMeta(filePath) {
1854
2334
  const content = await (0, import_promises4.readFile)(filePath, "utf-8");
1855
2335
  lines = content.split("\n").filter(Boolean);
1856
2336
  }
2337
+ let firstParsed = null;
2338
+ if (lines.length > 0) {
2339
+ try {
2340
+ firstParsed = JSON.parse(lines[0]);
2341
+ } catch {
2342
+ firstParsed = null;
2343
+ }
2344
+ }
2345
+ const isCodex = firstParsed?.type === "session_meta" || firstParsed?.type === "turn_context";
2346
+ if (isCodex) {
2347
+ if (isPartialRead) {
2348
+ const content = await (0, import_promises4.readFile)(filePath, "utf-8");
2349
+ lines = content.split("\n").filter(Boolean);
2350
+ }
2351
+ const meta = extractCodexMetadataFromLines(lines);
2352
+ return {
2353
+ sessionId: meta.sessionId,
2354
+ version: meta.version,
2355
+ gitBranch: meta.gitBranch,
2356
+ model: meta.model,
2357
+ slug: meta.slug,
2358
+ cwd: meta.cwd,
2359
+ firstUserMessage: meta.firstUserMessage,
2360
+ lastUserMessage: meta.lastUserMessage,
2361
+ timestamp: meta.timestamp,
2362
+ turnCount: meta.turnCount,
2363
+ lineCount: lines.length,
2364
+ branchedFrom: meta.branchedFrom
2365
+ };
2366
+ }
1857
2367
  let sessionId = "";
1858
2368
  let version = "";
1859
2369
  let gitBranch = "";
@@ -1950,6 +2460,30 @@ async function getSessionStatus(filePath) {
1950
2460
  } catch {
1951
2461
  continue;
1952
2462
  }
2463
+ if (obj.type === "event_msg") {
2464
+ const payload = obj.payload;
2465
+ switch (payload?.type) {
2466
+ case "task_complete":
2467
+ return { status: "completed" };
2468
+ case "task_started":
2469
+ return { status: "processing" };
2470
+ case "agent_message":
2471
+ return { status: "thinking" };
2472
+ case "token_count":
2473
+ continue;
2474
+ }
2475
+ }
2476
+ if (obj.type === "response_item") {
2477
+ const payload = obj.payload;
2478
+ if (payload?.type === "function_call") {
2479
+ return { status: "tool_use", toolName: payload.name };
2480
+ }
2481
+ if (payload?.type === "message") {
2482
+ const role = payload.role;
2483
+ if (role === "assistant") return { status: "thinking" };
2484
+ if (role === "user") return { status: "processing" };
2485
+ }
2486
+ }
1953
2487
  if (obj.type === "assistant" || obj.type === "user" || obj.type === "queue-operation") {
1954
2488
  meaningful.unshift(obj);
1955
2489
  const isEndTurn = obj.type === "assistant" && obj.message?.stop_reason === "end_turn";
@@ -1968,6 +2502,35 @@ async function getSessionStatus(filePath) {
1968
2502
  }
1969
2503
 
1970
2504
  // src/commands/sessions.ts
2505
+ var CODEX_SESSIONS_DIR = (0, import_node_path6.join)((0, import_node_os3.homedir)(), ".codex", "sessions");
2506
+ async function listCodexSessionFiles(cutoff) {
2507
+ const walk = async (dir, depth) => {
2508
+ if (depth > 4) return [];
2509
+ let entries;
2510
+ try {
2511
+ entries = await (0, import_promises5.readdir)(dir, { withFileTypes: true });
2512
+ } catch {
2513
+ return [];
2514
+ }
2515
+ const results = [];
2516
+ for (const entry of entries) {
2517
+ const filePath = (0, import_node_path6.join)(dir, entry.name);
2518
+ if (entry.isDirectory()) {
2519
+ results.push(...await walk(filePath, depth + 1));
2520
+ continue;
2521
+ }
2522
+ if (!entry.name.endsWith(".jsonl")) continue;
2523
+ try {
2524
+ const s = await (0, import_promises5.stat)(filePath);
2525
+ if (s.mtimeMs >= cutoff) results.push({ path: filePath, mtimeMs: s.mtimeMs });
2526
+ } catch {
2527
+ continue;
2528
+ }
2529
+ }
2530
+ return results;
2531
+ };
2532
+ return walk(CODEX_SESSIONS_DIR, 0);
2533
+ }
1971
2534
  async function listSessions(opts = {}) {
1972
2535
  const limit = Math.min(Math.max(1, opts.limit ?? 20), 100);
1973
2536
  const maxAgeMs = parseMaxAge(opts.maxAge ?? "7d");
@@ -2001,7 +2564,8 @@ async function listSessions(opts = {}) {
2001
2564
  }
2002
2565
  })
2003
2566
  );
2004
- const allFiles = nested.flat();
2567
+ const codexFiles = await listCodexSessionFiles(cutoff);
2568
+ const allFiles = [...nested.flat(), ...codexFiles];
2005
2569
  allFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
2006
2570
  const results = [];
2007
2571
  for (const file of allFiles) {
@@ -2024,7 +2588,8 @@ async function listSessions(opts = {}) {
2024
2588
  lastMessage: meta.lastUserMessage,
2025
2589
  turnCount: meta.turnCount,
2026
2590
  status: statusInfo.status,
2027
- mtime: file.mtimeMs
2591
+ mtime: file.mtimeMs,
2592
+ source: file.path.startsWith(CODEX_SESSIONS_DIR + "/") ? "codex" : "claude"
2028
2593
  });
2029
2594
  } catch {
2030
2595
  }
@@ -2038,10 +2603,9 @@ async function currentSession(cwd) {
2038
2603
  try {
2039
2604
  files = await (0, import_promises5.readdir)(projectDir);
2040
2605
  } catch {
2041
- return null;
2606
+ files = [];
2042
2607
  }
2043
2608
  const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
2044
- if (jsonlFiles.length === 0) return null;
2045
2609
  const statResults = await Promise.all(
2046
2610
  jsonlFiles.map(async (f) => {
2047
2611
  const filePath = (0, import_node_path6.join)(projectDir, f);
@@ -2053,7 +2617,21 @@ async function currentSession(cwd) {
2053
2617
  }
2054
2618
  })
2055
2619
  );
2056
- const valid = statResults.filter((r) => r !== null);
2620
+ const codexCandidates = await listCodexSessionFiles(0);
2621
+ const codexMatches = [];
2622
+ for (const file of codexCandidates) {
2623
+ try {
2624
+ const meta2 = await getSessionMeta(file.path);
2625
+ if (meta2.cwd === cwd) codexMatches.push(file);
2626
+ } catch {
2627
+ continue;
2628
+ }
2629
+ }
2630
+ const valid = [
2631
+ ...statResults.filter((r) => r !== null),
2632
+ ...codexMatches
2633
+ ];
2634
+ if (valid.length === 0) return null;
2057
2635
  valid.sort((a, b) => b.mtimeMs - a.mtimeMs);
2058
2636
  const latest = valid[0];
2059
2637
  if (!latest) return null;
@@ -2073,7 +2651,8 @@ async function currentSession(cwd) {
2073
2651
  lastMessage: meta.lastUserMessage,
2074
2652
  turnCount: meta.turnCount,
2075
2653
  status: statusInfo.status,
2076
- mtime: latest.mtimeMs
2654
+ mtime: latest.mtimeMs,
2655
+ source: latest.path.startsWith(CODEX_SESSIONS_DIR + "/") ? "codex" : "claude"
2077
2656
  };
2078
2657
  }
2079
2658