agent-ecosystem/src/main/utils/metadataExtraction.ts
iliya a30727d3b0 feat: enhance team file handling and logging improvements
- Added support for a new worker in the team file system to list teams, improving performance and reliability in team management.
- Introduced event loop lag monitoring in various IPC handlers to track and log slow operations, enhancing observability.
- Implemented caching for CLI installation status to reduce redundant calls and improve responsiveness.
- Updated project and team data services to include total session counts, optimizing data handling and performance.
- Enhanced error handling and logging across multiple services to provide clearer insights into performance issues and failures.

Made-with: Cursor
2026-03-03 22:00:11 +02:00

261 lines
6.2 KiB
TypeScript

/**
* Metadata extraction utilities for parsing first messages and session context from JSONL files.
*/
import { isCommandOutputContent, sanitizeDisplayContent } from '@shared/utils/contentSanitizer';
import { createLogger } from '@shared/utils/logger';
import * as readline from 'readline';
import { LocalFileSystemProvider } from '../services/infrastructure/LocalFileSystemProvider';
import { type ChatHistoryEntry, isTextContent, type UserEntry } from '../types';
import type { FileSystemProvider } from '../services/infrastructure/FileSystemProvider';
const logger = createLogger('Util:metadataExtraction');
const defaultProvider = new LocalFileSystemProvider();
const JSONL_HEAD_TIMEOUT_MS = 2000;
const JSONL_HEAD_MAX_BYTES = 256 * 1024;
const JSONL_HEAD_MAX_LINES = 400;
interface MessagePreview {
text: string;
timestamp: string;
isCommand: boolean;
}
function byteLen(chunk: string): number {
return Buffer.byteLength(chunk, 'utf8');
}
/**
* Extract CWD (current working directory) from the first entry.
* Used to get the actual project path from encoded directory names.
*/
export async function extractCwd(
filePath: string,
fsProvider: FileSystemProvider = defaultProvider
): Promise<string | null> {
if (!(await fsProvider.exists(filePath))) {
return null;
}
try {
const stat = await fsProvider.stat(filePath);
if (!stat.isFile()) {
return null;
}
} catch {
return null;
}
const fileStream = fsProvider.createReadStream(filePath, { encoding: 'utf8' });
let bytes = 0;
let timedOut = false;
const timer = setTimeout(() => {
timedOut = true;
fileStream.destroy();
}, JSONL_HEAD_TIMEOUT_MS);
fileStream.on('data', (chunk: string) => {
bytes += byteLen(chunk);
if (bytes > JSONL_HEAD_MAX_BYTES) {
fileStream.destroy();
}
});
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity,
});
try {
let lines = 0;
for await (const line of rl) {
if (++lines > JSONL_HEAD_MAX_LINES) {
break;
}
if (!line.trim()) continue;
let entry: ChatHistoryEntry;
try {
entry = JSON.parse(line) as ChatHistoryEntry;
} catch {
continue;
}
// Only conversational entries have cwd
if ('cwd' in entry && entry.cwd) {
rl.close();
fileStream.destroy();
return entry.cwd;
}
}
} catch (error) {
if (!timedOut) {
logger.debug(`Error extracting cwd from ${filePath}:`, error);
}
} finally {
clearTimeout(timer);
rl.close();
fileStream.destroy();
}
return null;
}
/**
* Extract a lightweight title preview from the first user message.
* For command-style sessions, falls back to a slash-command label.
*/
export async function extractFirstUserMessagePreview(
filePath: string,
fsProvider: FileSystemProvider = defaultProvider,
maxLines: number = 200
): Promise<{ text: string; timestamp: string } | null> {
const safeMaxLines = Math.max(1, maxLines);
try {
const stat = await fsProvider.stat(filePath);
if (!stat.isFile()) {
return null;
}
} catch {
return null;
}
const fileStream = fsProvider.createReadStream(filePath, { encoding: 'utf8' });
let bytes = 0;
let timedOut = false;
const timer = setTimeout(() => {
timedOut = true;
fileStream.destroy();
}, JSONL_HEAD_TIMEOUT_MS);
fileStream.on('data', (chunk: string) => {
bytes += byteLen(chunk);
if (bytes > JSONL_HEAD_MAX_BYTES) {
fileStream.destroy();
}
});
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity,
});
let commandFallback: { text: string; timestamp: string } | null = null;
let linesRead = 0;
try {
for await (const line of rl) {
if (linesRead++ >= safeMaxLines) {
break;
}
const trimmed = line.trim();
if (!trimmed) continue;
let entry: ChatHistoryEntry;
try {
entry = JSON.parse(trimmed) as ChatHistoryEntry;
} catch {
continue;
}
if (entry.type !== 'user') {
continue;
}
const preview = extractPreviewFromUserEntry(entry);
if (!preview) {
continue;
}
if (!preview.isCommand) {
return { text: preview.text, timestamp: preview.timestamp };
}
if (!commandFallback) {
commandFallback = { text: preview.text, timestamp: preview.timestamp };
}
}
} catch (error) {
if (!timedOut) {
logger.debug(`Error extracting first user preview from ${filePath}:`, error);
}
return commandFallback;
} finally {
clearTimeout(timer);
rl.close();
fileStream.destroy();
}
return commandFallback;
}
function extractPreviewFromUserEntry(entry: UserEntry): MessagePreview | null {
const timestamp = entry.timestamp ?? new Date().toISOString();
const message = entry.message;
if (!message) {
return null;
}
const content = message.content;
if (typeof content === 'string') {
if (isCommandOutputContent(content) || content.startsWith('[Request interrupted by user')) {
return null;
}
if (content.startsWith('<command-name>')) {
return {
text: extractCommandName(content),
timestamp,
isCommand: true,
};
}
const sanitized = sanitizeDisplayContent(content).trim();
if (!sanitized) {
return null;
}
return {
text: sanitized.substring(0, 500),
timestamp,
isCommand: false,
};
}
if (!Array.isArray(content)) {
return null;
}
const textContent = content
.filter(isTextContent)
.map((block) => block.text)
.join(' ')
.trim();
if (!textContent || textContent.startsWith('[Request interrupted by user')) {
return null;
}
if (textContent.startsWith('<command-name>')) {
return {
text: extractCommandName(textContent),
timestamp,
isCommand: true,
};
}
const sanitized = sanitizeDisplayContent(textContent).trim();
if (!sanitized) {
return null;
}
return {
text: sanitized.substring(0, 500),
timestamp,
isCommand: false,
};
}
function extractCommandName(content: string): string {
const commandMatch = /<command-name>\/([^<]+)<\/command-name>/.exec(content);
return commandMatch?.[1] ? `/${commandMatch[1]}` : '/command';
}