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/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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = '
|
|
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
|
-
|
|
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
|
|
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.
|
|
972
|
-
|
|
973
|
-
|
|
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
|
|
1046
|
-
if (
|
|
1047
|
-
const projectDir = (0, import_node_path.join)(projectsDir,
|
|
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 &&
|
|
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")
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|