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 SessionsPaginationOptions,
|
||||||
type WorktreeSource,
|
type WorktreeSource,
|
||||||
} from '@main/types';
|
} from '@main/types';
|
||||||
import {
|
import { analyzeSessionFileMetadata, extractCwd } from '@main/utils/jsonl';
|
||||||
analyzeSessionFileMetadata,
|
|
||||||
extractCwd,
|
|
||||||
extractFirstUserMessagePreview,
|
|
||||||
} from '@main/utils/jsonl';
|
|
||||||
import {
|
import {
|
||||||
buildSessionPath,
|
buildSessionPath,
|
||||||
buildSubagentsPath,
|
buildSubagentsPath,
|
||||||
|
|
@ -1035,20 +1031,30 @@ export class ProjectScanner {
|
||||||
const effectiveMtime = prefetchedMtimeMs ?? stats?.mtimeMs ?? Date.now();
|
const effectiveMtime = prefetchedMtimeMs ?? stats?.mtimeMs ?? Date.now();
|
||||||
const effectiveSize = prefetchedSize ?? stats?.size ?? -1;
|
const effectiveSize = prefetchedSize ?? stats?.size ?? -1;
|
||||||
const birthtimeMs = prefetchedBirthtimeMs ?? stats?.birthtimeMs ?? effectiveMtime;
|
const birthtimeMs = prefetchedBirthtimeMs ?? stats?.birthtimeMs ?? effectiveMtime;
|
||||||
const cachedPreview = this.sessionPreviewCache.get(filePath);
|
let metadata: Awaited<ReturnType<typeof analyzeSessionFileMetadata>>;
|
||||||
const preview =
|
const cachedMetadata = this.sessionMetadataCache.get(filePath);
|
||||||
cachedPreview?.mtimeMs === effectiveMtime && cachedPreview.size === effectiveSize
|
if (cachedMetadata?.mtimeMs === effectiveMtime && cachedMetadata.size === effectiveSize) {
|
||||||
? cachedPreview.preview
|
metadata = cachedMetadata.metadata;
|
||||||
: await this.extractLightPreviewWithRetry(filePath);
|
} else {
|
||||||
if (cachedPreview?.mtimeMs !== effectiveMtime || cachedPreview.size !== effectiveSize) {
|
try {
|
||||||
this.sessionPreviewCache.set(filePath, {
|
metadata = await analyzeSessionFileMetadata(filePath, this.fsProvider);
|
||||||
mtimeMs: effectiveMtime,
|
this.sessionMetadataCache.set(filePath, {
|
||||||
size: effectiveSize,
|
mtimeMs: effectiveMtime,
|
||||||
preview,
|
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 metadataLevel: SessionMetadataLevel = 'light';
|
||||||
const previewTimestampMs = this.parseTimestampMs(preview?.timestamp);
|
const previewTimestampMs = this.parseTimestampMs(metadata.firstUserMessage?.timestamp);
|
||||||
const createdAt =
|
const createdAt =
|
||||||
previewTimestampMs !== null && Number.isFinite(previewTimestampMs)
|
previewTimestampMs !== null && Number.isFinite(previewTimestampMs)
|
||||||
? previewTimestampMs
|
? previewTimestampMs
|
||||||
|
|
@ -1059,10 +1065,10 @@ export class ProjectScanner {
|
||||||
projectId,
|
projectId,
|
||||||
projectPath,
|
projectPath,
|
||||||
createdAt: Math.floor(createdAt),
|
createdAt: Math.floor(createdAt),
|
||||||
firstMessage: preview?.text,
|
firstMessage: metadata.firstUserMessage?.text,
|
||||||
messageTimestamp: preview?.timestamp,
|
messageTimestamp: metadata.firstUserMessage?.timestamp,
|
||||||
hasSubagents: false,
|
hasSubagents: false,
|
||||||
messageCount: 0,
|
messageCount: metadata.messageCount,
|
||||||
metadataLevel,
|
metadataLevel,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1459,31 +1465,6 @@ export class ProjectScanner {
|
||||||
return results;
|
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 {
|
private getErrorCode(error: unknown): string {
|
||||||
if (typeof error === 'object' && error !== null && 'code' in error) {
|
if (typeof error === 'object' && error !== null && 'code' in error) {
|
||||||
const code = (error as { code?: unknown }).code;
|
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