@@ -4515,7 +4515,10 @@ function listCodexSessions(limit, options = {}) {
45154515 titleReadBytes
45164516 });
45174517 if (summary) {
4518- sessions.push(summary);
4518+ sessions.push({
4519+ ...summary,
4520+ derived: isDerivedSessionFile(filePath)
4521+ });
45194522 }
45204523
45214524 if (sessions.length >= targetCount) {
@@ -4697,6 +4700,7 @@ function listClaudeSessions(limit, options = {}) {
46974700 models,
46984701 __messageCountExact: quickRecords.length > 0 && isSessionSummaryMessageCountExact(fileStat, summaryReadBytes),
46994702 filePath,
4703+ derived: isDerivedSessionFile(filePath),
47004704 keywords,
47014705 capabilities
47024706 });
@@ -4723,7 +4727,10 @@ function listClaudeSessions(limit, options = {}) {
47234727 titleReadBytes
47244728 });
47254729 if (summary) {
4726- sessions.push(summary);
4730+ sessions.push({
4731+ ...summary,
4732+ derived: isDerivedSessionFile(filePath)
4733+ });
47274734 }
47284735
47294736 if (sessions.length >= targetCount) {
@@ -4745,7 +4752,10 @@ function listClaudeSessions(limit, options = {}) {
47454752 titleReadBytes
47464753 });
47474754 if (summary) {
4748- sessions.push(summary);
4755+ sessions.push({
4756+ ...summary,
4757+ derived: isDerivedSessionFile(filePath)
4758+ });
47494759 }
47504760 seen.add(filePath);
47514761 }
@@ -5210,6 +5220,65 @@ function findClaudeSessionIndexPath(sessionFilePath) {
52105220 return '';
52115221}
52125222
5223+ function resolveClaudeProjectDirForCwd(cwd) {
5224+ const projectsDir = getClaudeProjectsDir();
5225+ const raw = typeof cwd === 'string' ? cwd.trim() : '';
5226+ if (!projectsDir || !raw) {
5227+ return '';
5228+ }
5229+ const ignoreCase = process.platform === 'win32';
5230+ const resolvedCwd = path.resolve(expandHomePath(raw));
5231+ let entries = [];
5232+ try {
5233+ entries = fs.readdirSync(projectsDir, { withFileTypes: true });
5234+ } catch (_) {
5235+ entries = [];
5236+ }
5237+ for (const entry of entries) {
5238+ if (!entry || !entry.isDirectory()) continue;
5239+ const projectDir = path.join(projectsDir, entry.name);
5240+ const indexPath = path.join(projectDir, 'sessions-index.json');
5241+ if (!fs.existsSync(indexPath)) continue;
5242+ const index = readJsonFile(indexPath, null);
5243+ const originalPathRaw = index && typeof index.originalPath === 'string' ? index.originalPath.trim() : '';
5244+ if (!originalPathRaw) continue;
5245+ const resolvedOriginal = path.resolve(expandHomePath(originalPathRaw));
5246+ if (normalizePathForCompare(resolvedOriginal, { ignoreCase }) === normalizePathForCompare(resolvedCwd, { ignoreCase })) {
5247+ return projectDir;
5248+ }
5249+ }
5250+ const hash = crypto.createHash('sha1').update(resolvedCwd).digest('hex').slice(0, 12);
5251+ return path.join(projectsDir, `codexmate-${hash}`);
5252+ }
5253+
5254+ function ensureClaudeSessionsIndex(indexPath, originalPath) {
5255+ if (!indexPath) return;
5256+ const resolvedOriginal = typeof originalPath === 'string' && originalPath.trim()
5257+ ? path.resolve(expandHomePath(originalPath.trim()))
5258+ : '';
5259+ const existing = readJsonFile(indexPath, null);
5260+ const index = existing && typeof existing === 'object' && !Array.isArray(existing)
5261+ ? { ...existing }
5262+ : { entries: [] };
5263+ if (!Array.isArray(index.entries)) {
5264+ index.entries = [];
5265+ }
5266+ if (!index.originalPath && resolvedOriginal) {
5267+ index.originalPath = resolvedOriginal;
5268+ }
5269+ if (!fs.existsSync(indexPath)) {
5270+ if (!index.originalPath) {
5271+ index.originalPath = resolvedOriginal || path.dirname(indexPath);
5272+ }
5273+ writeJsonAtomic(indexPath, index);
5274+ return;
5275+ }
5276+ if (existing && typeof existing === 'object' && !Array.isArray(existing) && existing.originalPath === index.originalPath) {
5277+ return;
5278+ }
5279+ writeJsonAtomic(indexPath, index);
5280+ }
5281+
52135282const {
52145283 findAvailablePort,
52155284 saveBuiltinProxySettings,
@@ -6265,6 +6334,28 @@ function buildSessionPlainText(messages) {
62656334 return lines.join('\n');
62666335}
62676336
6337+ function getDerivedSessionMetaPath(filePath) {
6338+ if (!filePath) return '';
6339+ const base = filePath.toLowerCase().endsWith('.jsonl')
6340+ ? filePath.slice(0, -5)
6341+ : filePath;
6342+ return `${base}.meta.json`;
6343+ }
6344+
6345+ function isDerivedSessionFile(filePath) {
6346+ const metaPath = getDerivedSessionMetaPath(filePath);
6347+ if (!metaPath) return false;
6348+ try {
6349+ if (fs.existsSync(metaPath)) {
6350+ return true;
6351+ }
6352+ } catch (_) {
6353+ return false;
6354+ }
6355+ const base = path.basename(filePath || '', path.extname(filePath || ''));
6356+ return /-\d{8}-\d{6}-[0-9a-f]{6}$/i.test(base);
6357+ }
6358+
62686359function resolveStateMaxMessages(state) {
62696360 if (!state || typeof state !== 'object') {
62706361 return MAX_EXPORT_MESSAGES;
@@ -6610,6 +6701,20 @@ async function readSessionDetail(params = {}) {
66106701 sessionId,
66116702 cwd: extracted.cwd || '',
66126703 updatedAt: extracted.updatedAt || '',
6704+ derived: (() => {
6705+ try {
6706+ const metaPath = filePath.toLowerCase().endsWith('.jsonl')
6707+ ? `${filePath.slice(0, -5)}.meta.json`
6708+ : `${filePath}.meta.json`;
6709+ if (fs.existsSync(metaPath)) {
6710+ return true;
6711+ }
6712+ } catch (_) {
6713+ return false;
6714+ }
6715+ const base = path.basename(filePath || '', path.extname(filePath || ''));
6716+ return /-\d{8}-\d{6}-[0-9a-f]{6}$/i.test(base);
6717+ })(),
66136718 totalMessages: hasExactTotalMessages ? extracted.totalMessages : null,
66146719 clipped: typeof extracted.clipped === 'boolean'
66156720 ? extracted.clipped
@@ -6637,6 +6742,15 @@ async function readSessionPlain(params = {}) {
66376742 return { error: 'Session file not found' };
66386743 }
66396744
6745+ const rawMaxMessages = params.maxMessages;
6746+ const maxMessages = rawMaxMessages === Infinity || rawMaxMessages === 'all'
6747+ ? Infinity
6748+ : (
6749+ Number.isFinite(Number(rawMaxMessages))
6750+ ? Math.max(1, Math.floor(Number(rawMaxMessages)))
6751+ : 50
6752+ );
6753+
66406754 let extracted;
66416755 if (source === 'gemini') {
66426756 let json;
@@ -6657,15 +6771,19 @@ async function readSessionPlain(params = {}) {
66576771 const text = extractMessageText(extractGeminiMessageText(entry.content ?? entry.message ?? entry.text));
66586772 if (!text && role !== 'system') continue;
66596773 messages.push({ role, text });
6774+ if (maxMessages !== Infinity && messages.length >= maxMessages) {
6775+ break;
6776+ }
66606777 }
66616778 extracted = {
66626779 sessionId: typeof json.sessionId === 'string' && json.sessionId.trim() ? json.sessionId.trim() : path.basename(filePath, '.json'),
66636780 cwd: typeof json.projectRoot === 'string' ? json.projectRoot : '',
6664- messages
6781+ messages,
6782+ truncated: maxMessages !== Infinity && rawMessages.length > messages.length
66656783 };
66666784 } else {
66676785 try {
6668- extracted = await extractMessagesFromFile(filePath, source, { maxMessages: Infinity });
6786+ extracted = await extractMessagesFromFile(filePath, source, { maxMessages });
66696787 } catch (e) {
66706788 extracted = null;
66716789 }
@@ -6679,7 +6797,7 @@ async function readSessionPlain(params = {}) {
66796797 if (fallbackRecords.length === 0) {
66806798 return { error: 'Session file is empty' };
66816799 }
6682- extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages: Infinity });
6800+ extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
66836801 }
66846802 }
66856803
@@ -6698,7 +6816,8 @@ async function readSessionPlain(params = {}) {
66986816 sessionId,
66996817 title: sessionId,
67006818 filePath,
6701- text
6819+ text,
6820+ clipped: maxMessages !== Infinity && !!(extracted && extracted.truncated)
67026821 };
67036822}
67046823
@@ -6838,13 +6957,14 @@ async function convertSessionToDerived(params = {}) {
68386957 const outputDir = target === 'codex'
68396958 ? getCodexSessionsDir()
68406959 : (target === 'claude'
6841- ? path.join(getClaudeProjectsDir(), 'codexmate-derived')
6960+ ? (resolveClaudeProjectDirForCwd(extracted.cwd || '') || path.join(getClaudeProjectsDir(), 'codexmate-derived') )
68426961 : buildDerivedSessionOutputDir(target, source, sourceKey));
68436962 ensureDir(outputDir);
68446963 const outputPath = path.join(outputDir, `${derivedSessionId}.jsonl`);
68456964 const metaPath = path.join(outputDir, `${derivedSessionId}.meta.json`);
68466965
68476966 const cwd = typeof extracted.cwd === 'string' ? extracted.cwd : '';
6967+ const resolvedCwd = cwd ? path.resolve(expandHomePath(cwd)) : '';
68486968 const messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
68496969 const now = Date.now();
68506970 const baseTime = new Date(now).toISOString();
@@ -6866,6 +6986,7 @@ async function convertSessionToDerived(params = {}) {
68666986 }));
68676987 }
68686988 } else {
6989+ const claudeIndexPath = target === 'claude' ? path.join(outputDir, 'sessions-index.json') : '';
68696990 for (let i = 0; i < messages.length; i += 1) {
68706991 const message = messages[i];
68716992 if (!message) continue;
@@ -6881,6 +7002,9 @@ async function convertSessionToDerived(params = {}) {
68817002 message: { content: text }
68827003 }));
68837004 }
7005+ if (claudeIndexPath) {
7006+ ensureClaudeSessionsIndex(claudeIndexPath, resolvedCwd);
7007+ }
68847008 }
68857009
68867010 fs.writeFileSync(outputPath, `${lines.join('\n')}\n`, 'utf-8');
@@ -6909,6 +7033,7 @@ async function convertSessionToDerived(params = {}) {
69097033 : parseClaudeSessionSummary(outputPath, { summaryReadBytes: SESSION_BROWSE_SUMMARY_READ_BYTES, titleReadBytes: SESSION_BROWSE_SUMMARY_READ_BYTES });
69107034 if (target === 'claude' && summary) {
69117035 const indexPath = path.join(outputDir, 'sessions-index.json');
7036+ ensureClaudeSessionsIndex(indexPath, resolvedCwd);
69127037 upsertClaudeSessionIndexEntry(indexPath, outputPath, {
69137038 source: 'claude',
69147039 trashId: summary.sessionId,
@@ -6921,7 +7046,8 @@ async function convertSessionToDerived(params = {}) {
69217046 messageCount: summary.messageCount,
69227047 provider: summary.provider,
69237048 keywords: summary.keywords,
6924- capabilities: summary.capabilities
7049+ capabilities: summary.capabilities,
7050+ claudeIndexEntry: resolvedCwd ? { projectPath: resolvedCwd } : null
69257051 });
69267052 }
69277053 const maxMessagesLabel = maxMessages === Infinity ? 'all' : maxMessages;
@@ -6932,7 +7058,7 @@ async function convertSessionToDerived(params = {}) {
69327058 target,
69337059 truncated: !!extracted.truncated,
69347060 maxMessages: maxMessagesLabel,
6935- session: summary || {
7061+ session: summary ? { ...summary, derived: true } : {
69367062 source: target,
69377063 sourceLabel: target === 'codex' ? 'Codex' : 'Claude Code',
69387064 sessionId: derivedSessionId,
@@ -6949,6 +7075,7 @@ async function convertSessionToDerived(params = {}) {
69497075 reasoningOutputTokens: 0,
69507076 __messageCountExact: true,
69517077 filePath: outputPath,
7078+ derived: true,
69527079 keywords: [],
69537080 capabilities: {}
69547081 }
0 commit comments