fix: Claude resume候補をjsonlフォールバックで取得
This commit is contained in:
parent
134b666480
commit
a5e2badc0b
@ -26,6 +26,30 @@ describe('loadSessionIndex', () => {
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('falls back to jsonl files when sessions-index.json does not exist', () => {
|
||||
const filePath = join(mockSessionsDir, 'fallback-session.jsonl');
|
||||
writeFileSync(filePath, [
|
||||
JSON.stringify({
|
||||
type: 'user',
|
||||
gitBranch: 'develop',
|
||||
message: { content: [{ type: 'text', text: 'Resume me from jsonl fallback' }] },
|
||||
isSidechain: false,
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'assistant',
|
||||
message: { content: [{ type: 'text', text: 'Sure' }] },
|
||||
}),
|
||||
].join('\n'));
|
||||
|
||||
const result = loadSessionIndex('/any');
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]!.sessionId).toBe('fallback-session');
|
||||
expect(result[0]!.firstPrompt).toBe('Resume me from jsonl fallback');
|
||||
expect(result[0]!.messageCount).toBe(2);
|
||||
expect(result[0]!.gitBranch).toBe('develop');
|
||||
});
|
||||
|
||||
it('reads and parses sessions-index.json correctly', () => {
|
||||
const indexData = {
|
||||
version: 1,
|
||||
@ -105,6 +129,36 @@ describe('loadSessionIndex', () => {
|
||||
const result = loadSessionIndex('/any');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('uses jsonl fallback when sessions-index.json is corrupted', () => {
|
||||
writeFileSync(join(mockSessionsDir, 'sessions-index.json'), '{corrupted json content');
|
||||
writeFileSync(join(mockSessionsDir, 'fallback-on-corrupt.jsonl'), JSON.stringify({
|
||||
type: 'user',
|
||||
message: { content: [{ type: 'text', text: 'Fallback on corrupted index' }] },
|
||||
isSidechain: false,
|
||||
}));
|
||||
|
||||
const result = loadSessionIndex('/any');
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]!.sessionId).toBe('fallback-on-corrupt');
|
||||
});
|
||||
|
||||
it('filters out sidechain sessions in jsonl fallback', () => {
|
||||
writeFileSync(join(mockSessionsDir, 'main-session.jsonl'), JSON.stringify({
|
||||
type: 'user',
|
||||
message: { content: [{ type: 'text', text: 'Main session' }] },
|
||||
isSidechain: false,
|
||||
}));
|
||||
writeFileSync(join(mockSessionsDir, 'sidechain-session.jsonl'), JSON.stringify({
|
||||
type: 'user',
|
||||
message: { content: [{ type: 'text', text: 'Sidechain session' }] },
|
||||
isSidechain: true,
|
||||
}));
|
||||
|
||||
const result = loadSessionIndex('/any');
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]!.sessionId).toBe('main-session');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractLastAssistantResponse', () => {
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
* to extract session metadata and last assistant responses.
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { getClaudeProjectSessionsDir } from '../config/project/sessionStore.js';
|
||||
|
||||
@ -26,6 +26,99 @@ interface SessionsIndex {
|
||||
entries: SessionIndexEntry[];
|
||||
}
|
||||
|
||||
interface SessionMessageContent {
|
||||
type: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
interface SessionMessage {
|
||||
content?: SessionMessageContent[];
|
||||
}
|
||||
|
||||
interface SessionJsonlRecord {
|
||||
type?: string;
|
||||
sessionId?: string;
|
||||
message?: SessionMessage;
|
||||
timestamp?: string;
|
||||
gitBranch?: string;
|
||||
isSidechain?: boolean;
|
||||
}
|
||||
|
||||
function buildEntryFromJsonlFile(sessionsDir: string, fileName: string): SessionIndexEntry | null {
|
||||
const fullPath = join(sessionsDir, fileName);
|
||||
const sessionId = fileName.replace(/\.jsonl$/, '');
|
||||
|
||||
if (!sessionId || sessionId === fileName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let firstPrompt = '';
|
||||
let messageCount = 0;
|
||||
let gitBranch = '';
|
||||
let isSidechain = false;
|
||||
|
||||
try {
|
||||
const content = readFileSync(fullPath, 'utf-8');
|
||||
const lines = content.split('\n').filter((line) => line.trim().length > 0);
|
||||
for (const line of lines) {
|
||||
let record: SessionJsonlRecord;
|
||||
try {
|
||||
record = JSON.parse(line) as SessionJsonlRecord;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (record.type === 'user' || record.type === 'assistant') {
|
||||
messageCount += 1;
|
||||
}
|
||||
|
||||
if (!gitBranch && typeof record.gitBranch === 'string') {
|
||||
gitBranch = record.gitBranch;
|
||||
}
|
||||
|
||||
if (record.isSidechain === true) {
|
||||
isSidechain = true;
|
||||
}
|
||||
|
||||
if (!firstPrompt && record.type === 'user' && Array.isArray(record.message?.content)) {
|
||||
const textBlock = record.message.content.find((block) => block.type === 'text' && typeof block.text === 'string');
|
||||
if (textBlock?.text) {
|
||||
firstPrompt = textBlock.text.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const modified = statSync(fullPath).mtime.toISOString();
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
firstPrompt: firstPrompt || sessionId,
|
||||
modified,
|
||||
messageCount,
|
||||
gitBranch,
|
||||
isSidechain,
|
||||
fullPath,
|
||||
};
|
||||
}
|
||||
|
||||
function loadSessionIndexFromJsonl(sessionsDir: string): SessionIndexEntry[] {
|
||||
if (!existsSync(sessionsDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const jsonlFiles = readdirSync(sessionsDir)
|
||||
.filter((name) => name.endsWith('.jsonl'));
|
||||
|
||||
return jsonlFiles
|
||||
.map((fileName) => buildEntryFromJsonlFile(sessionsDir, fileName))
|
||||
.filter((entry): entry is SessionIndexEntry => entry !== null)
|
||||
.filter((entry) => !entry.isSidechain)
|
||||
.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the session index for a project directory.
|
||||
*
|
||||
@ -37,7 +130,7 @@ export function loadSessionIndex(projectDir: string): SessionIndexEntry[] {
|
||||
const indexPath = join(sessionsDir, 'sessions-index.json');
|
||||
|
||||
if (!existsSync(indexPath)) {
|
||||
return [];
|
||||
return loadSessionIndexFromJsonl(sessionsDir);
|
||||
}
|
||||
|
||||
const content = readFileSync(indexPath, 'utf-8');
|
||||
@ -46,11 +139,11 @@ export function loadSessionIndex(projectDir: string): SessionIndexEntry[] {
|
||||
try {
|
||||
index = JSON.parse(content) as SessionsIndex;
|
||||
} catch {
|
||||
return [];
|
||||
return loadSessionIndexFromJsonl(sessionsDir);
|
||||
}
|
||||
|
||||
if (!index.entries || !Array.isArray(index.entries)) {
|
||||
return [];
|
||||
return loadSessionIndexFromJsonl(sessionsDir);
|
||||
}
|
||||
|
||||
return index.entries
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user