fix(scanner): preserve message count in light metadata

This commit is contained in:
777genius 2026-04-11 09:07:25 +03:00
parent fafaca4a38
commit 92dbae84ec
2 changed files with 98 additions and 45 deletions

View file

@ -39,11 +39,7 @@ import {
type SessionsPaginationOptions,
type WorktreeSource,
} from '@main/types';
import {
analyzeSessionFileMetadata,
extractCwd,
extractFirstUserMessagePreview,
} from '@main/utils/jsonl';
import { analyzeSessionFileMetadata, extractCwd } from '@main/utils/jsonl';
import {
buildSessionPath,
buildSubagentsPath,
@ -1035,20 +1031,30 @@ export class ProjectScanner {
const effectiveMtime = prefetchedMtimeMs ?? stats?.mtimeMs ?? Date.now();
const effectiveSize = prefetchedSize ?? stats?.size ?? -1;
const birthtimeMs = prefetchedBirthtimeMs ?? stats?.birthtimeMs ?? effectiveMtime;
const cachedPreview = this.sessionPreviewCache.get(filePath);
const preview =
cachedPreview?.mtimeMs === effectiveMtime && cachedPreview.size === effectiveSize
? cachedPreview.preview
: await this.extractLightPreviewWithRetry(filePath);
if (cachedPreview?.mtimeMs !== effectiveMtime || cachedPreview.size !== effectiveSize) {
this.sessionPreviewCache.set(filePath, {
mtimeMs: effectiveMtime,
size: effectiveSize,
preview,
});
let metadata: Awaited<ReturnType<typeof analyzeSessionFileMetadata>>;
const cachedMetadata = this.sessionMetadataCache.get(filePath);
if (cachedMetadata?.mtimeMs === effectiveMtime && cachedMetadata.size === effectiveSize) {
metadata = cachedMetadata.metadata;
} else {
try {
metadata = await analyzeSessionFileMetadata(filePath, this.fsProvider);
this.sessionMetadataCache.set(filePath, {
mtimeMs: effectiveMtime,
size: effectiveSize,
metadata,
});
} catch (error) {
logger.debug(`Failed to analyze session metadata for ${filePath}:`, error);
metadata = {
firstUserMessage: null,
messageCount: 0,
isOngoing: false,
gitBranch: null,
};
}
}
const metadataLevel: SessionMetadataLevel = 'light';
const previewTimestampMs = this.parseTimestampMs(preview?.timestamp);
const previewTimestampMs = this.parseTimestampMs(metadata.firstUserMessage?.timestamp);
const createdAt =
previewTimestampMs !== null && Number.isFinite(previewTimestampMs)
? previewTimestampMs
@ -1059,10 +1065,10 @@ export class ProjectScanner {
projectId,
projectPath,
createdAt: Math.floor(createdAt),
firstMessage: preview?.text,
messageTimestamp: preview?.timestamp,
firstMessage: metadata.firstUserMessage?.text,
messageTimestamp: metadata.firstUserMessage?.timestamp,
hasSubagents: false,
messageCount: 0,
messageCount: metadata.messageCount,
metadataLevel,
};
}
@ -1459,31 +1465,6 @@ export class ProjectScanner {
return results;
}
private async extractLightPreviewWithRetry(
filePath: string
): Promise<{ text: string; timestamp: string } | null> {
const maxAttempts = this.fsProvider.type === 'ssh' ? 3 : 1;
let lastError: unknown;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await extractFirstUserMessagePreview(filePath, this.fsProvider);
} catch (error) {
lastError = error;
if (attempt < maxAttempts && this.isTransientFsError(error)) {
await this.sleep(50 * attempt);
continue;
}
break;
}
}
if (lastError) {
logger.debug(`Failed to extract light preview for ${filePath}:`, lastError);
}
return null;
}
private getErrorCode(error: unknown): string {
if (typeof error === 'object' && error !== null && 'code' in error) {
const code = (error as { code?: unknown }).code;

View file

@ -0,0 +1,72 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, describe, expect, it } from 'vitest';
import { ProjectScanner } from '../../../../src/main/services/discovery/ProjectScanner';
describe('ProjectScanner light metadata', () => {
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs) {
try {
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
} catch {
// Ignore cleanup failures
}
}
tempDirs.length = 0;
});
it('reuses analyzed metadata so light sessions expose the real message count', async () => {
const projectsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'scanner-light-'));
tempDirs.push(projectsDir);
const encodedName = '-Users-test-myproject';
const projectDir = path.join(projectsDir, encodedName);
fs.mkdirSync(projectDir);
const filePath = path.join(projectDir, 'session.jsonl');
fs.writeFileSync(
filePath,
[
JSON.stringify({
type: 'user',
uuid: 'u1',
cwd: '/Users/test/myproject',
timestamp: '2026-01-01T00:00:00.000Z',
message: { role: 'user', content: 'hello world' },
}),
JSON.stringify({
type: 'assistant',
uuid: 'a1',
timestamp: '2026-01-01T00:00:01.000Z',
message: { role: 'assistant', content: 'hi there' },
}),
].join('\n') + '\n',
'utf8'
);
const scanner = new ProjectScanner(projectsDir);
const session = await (
scanner as unknown as {
buildLightSessionMetadata: (
projectId: string,
sessionId: string,
filePath: string,
projectPath: string
) => Promise<{ messageCount: number; firstMessage?: string; metadataLevel: string }>;
}
).buildLightSessionMetadata(
encodedName,
'session',
filePath,
'/Users/test/myproject'
);
expect(session.metadataLevel).toBe('light');
expect(session.firstMessage).toBe('hello world');
expect(session.messageCount).toBe(2);
});
});