fix(scanner): preserve message count in light metadata
This commit is contained in:
parent
fafaca4a38
commit
92dbae84ec
2 changed files with 98 additions and 45 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue