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.
- package/dist/cli.js +674 -95
- package/dist/index.js +686 -95
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = '
|
|
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
|
-
|
|
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
|
|
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.
|
|
886
|
-
|
|
887
|
-
|
|
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
|
|
960
|
-
if (
|
|
961
|
-
const projectDir = (0, import_node_path.join)(projectsDir,
|
|
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 &&
|
|
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")
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|