cc-haha/scripts/perf/local-index-corpus.ts
程序员阿江(Relakkes) 53a15098c0 perf(desktop): add SQLite-backed local data index #1016
Project sessions, activity, scheduled runs, traces, and searchable content into rebuildable managed databases while preserving canonical-file fallbacks. Add diagnostics, recovery, parity coverage, and performance acceptance checks.
2026-07-16 04:16:02 +08:00

596 lines
17 KiB
TypeScript

import { createHash } from 'node:crypto'
import { createReadStream } from 'node:fs'
import {
mkdir,
open,
stat,
utimes,
writeFile,
} from 'node:fs/promises'
import { join, relative, resolve, sep } from 'node:path'
export type LocalIndexCorpusOptions = {
rootDir: string
sessions: number
entriesPerSession: number
largeTranscriptBytes?: number
seed: number
}
export type LocalIndexCorpus = {
configDir: string
projectsDir: string
transcriptPaths: string[]
manifestPath: string
}
type JsonRecord = Record<string, unknown>
type SessionSummary = {
sourcePath: string
sessionId: string
title: string
createdAt: string
modifiedAt: string
messageCount: number
}
type SourceManifestEntry = {
path: string
bytes: number
sha256: string
}
const PROJECT_COUNT = 8
const LARGE_RECORD_CHUNK_BYTES = 1024 * 1024
const SYNTHETIC_MODEL = 'claude-synthetic-benchmark'
export type BufferWriter = {
write: (
buffer: Uint8Array,
offset: number,
length: number,
) => Promise<{ bytesWritten: number }>
}
export async function writeBufferFully(
writer: BufferWriter,
buffer: Uint8Array,
): Promise<number> {
let offset = 0
while (offset < buffer.byteLength) {
const remaining = buffer.byteLength - offset
const { bytesWritten } = await writer.write(buffer, offset, remaining)
if (
!Number.isSafeInteger(bytesWritten) ||
bytesWritten < 1 ||
bytesWritten > remaining
) {
throw new Error(`invalid short-write result: ${bytesWritten} of ${remaining} bytes`)
}
offset += bytesWritten
}
return offset
}
function assertOptions(options: LocalIndexCorpusOptions): void {
if (!options.rootDir.trim()) {
throw new Error('rootDir must not be empty')
}
if (!Number.isSafeInteger(options.sessions) || options.sessions < 1) {
throw new Error('sessions must be a positive safe integer')
}
if (
!Number.isSafeInteger(options.entriesPerSession) ||
options.entriesPerSession < 6
) {
throw new Error('entriesPerSession must be a safe integer of at least 6')
}
if (!Number.isSafeInteger(options.seed)) {
throw new Error('seed must be a safe integer')
}
if (
options.largeTranscriptBytes !== undefined &&
(!Number.isSafeInteger(options.largeTranscriptBytes) ||
options.largeTranscriptBytes < 0)
) {
throw new Error('largeTranscriptBytes must be a non-negative safe integer')
}
}
function seededHex(seed: number, label: string): string {
return createHash('sha256')
.update(`cc-haha-local-index:${seed}:${label}`)
.digest('hex')
}
function sessionIdFor(seed: number, index: number): string {
const hex = seededHex(seed, `session:${index}`)
return [
hex.slice(0, 8),
hex.slice(8, 12),
hex.slice(12, 16),
hex.slice(16, 20),
hex.slice(20, 32),
].join('-')
}
function entryId(seed: number, sessionIndex: number, label: string): string {
return sessionIdFor(seed, sessionIndex * 100 + Number.parseInt(
seededHex(seed, label).slice(0, 6),
16,
))
}
function timestampFor(seed: number, sessionIndex: number, offsetSeconds: number): string {
const seedDayOffset = Math.abs(seed) % 300
const timestamp =
Date.UTC(2025, 0, 1 + seedDayOffset) +
sessionIndex * 60_000 +
offsetSeconds * 1000
return new Date(timestamp).toISOString()
}
function projectIndexFor(sessionIndex: number, sessions: number): number {
if (sessions > 1 && sessionIndex === sessions - 1) {
return 1
}
return sessionIndex % PROJECT_COUNT
}
function projectDirFor(projectIndex: number): string {
return `-synthetic-local-index-project-${projectIndex.toString().padStart(2, '0')}`
}
function syntheticWorkDir(projectIndex: number): string {
return `/synthetic/local-index/project-${projectIndex.toString().padStart(2, '0')}`
}
function messageUsage(sessionIndex: number): {
input_tokens: number
output_tokens: number
cache_read_input_tokens: number
cache_creation_input_tokens: number
} {
return {
input_tokens: 10 + sessionIndex % 5,
output_tokens: 4 + sessionIndex % 3,
cache_read_input_tokens: sessionIndex % 7,
cache_creation_input_tokens: sessionIndex % 2,
}
}
function usageTotal(usage: ReturnType<typeof messageUsage>): number {
return Object.values(usage).reduce((total, value) => total + value, 0)
}
function userRecord(
seed: number,
sessionIndex: number,
sessionId: string,
workDir: string,
currentShape: boolean,
): JsonRecord {
const text = currentShape
? `Synthetic current-shape prompt ${sessionIndex}`
: `Synthetic old-shape prompt ${sessionIndex}`
return {
parentUuid: null,
isSidechain: false,
type: 'user',
message: {
role: 'user',
content: currentShape ? [{ type: 'text', text }] : text,
},
uuid: entryId(seed, sessionIndex, 'user'),
timestamp: timestampFor(seed, sessionIndex, 10),
userType: 'external',
cwd: workDir,
sessionId,
}
}
function assistantRecord(
seed: number,
sessionIndex: number,
parentUuid: string,
content: Array<Record<string, unknown>> = [{
type: 'text',
text: `Synthetic assistant response ${sessionIndex}`,
}],
offsetSeconds = 20,
): JsonRecord {
return {
parentUuid,
isSidechain: false,
type: 'assistant',
message: {
model: SYNTHETIC_MODEL,
id: `msg_${seededHex(seed, `message:${sessionIndex}:${offsetSeconds}`).slice(0, 20)}`,
type: 'message',
role: 'assistant',
content,
usage: messageUsage(sessionIndex),
},
uuid: entryId(seed, sessionIndex, `assistant:${offsetSeconds}`),
timestamp: timestampFor(seed, sessionIndex, offsetSeconds),
}
}
function sourcePath(configDir: string, filePath: string): string {
return relative(configDir, filePath).split(sep).join('/')
}
async function streamLargeTranscript(
filePath: string,
requestedBytes: number,
): Promise<number> {
let currentBytes = (await stat(filePath)).size
if (currentBytes >= requestedBytes) return currentBytes
const record = Buffer.from(`${JSON.stringify({
type: 'progress',
marker: 'synthetic-large-record',
data: 'x'.repeat(960),
})}\n`)
const recordsPerChunk = Math.max(
1,
Math.floor(LARGE_RECORD_CHUNK_BYTES / record.length),
)
const chunk = Buffer.concat(Array.from({ length: recordsPerChunk }, () => record))
const handle = await open(filePath, 'a')
try {
while (currentBytes + chunk.length < requestedBytes) {
currentBytes += await writeBufferFully(handle, chunk)
}
while (currentBytes < requestedBytes) {
currentBytes += await writeBufferFully(handle, record)
}
} finally {
await handle.close()
}
return currentBytes
}
async function hashSource(configDir: string, filePath: string): Promise<SourceManifestEntry> {
const hash = createHash('sha256')
let bytes = 0
for await (const chunk of createReadStream(filePath)) {
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
hash.update(buffer)
bytes += buffer.length
}
return {
path: sourcePath(configDir, filePath),
bytes,
sha256: hash.digest('hex'),
}
}
export async function createLocalIndexCorpus(
options: LocalIndexCorpusOptions,
): Promise<LocalIndexCorpus> {
assertOptions(options)
const rootDir = resolve(options.rootDir)
const configDir = join(rootDir, 'home', '.claude')
const projectsDir = join(configDir, 'projects')
const manifestPath = join(rootDir, 'local-index-corpus-manifest.json')
const largeTranscriptBytes = options.largeTranscriptBytes ?? 0
await mkdir(projectsDir, { recursive: true })
const transcriptPaths: string[] = []
const sourcePaths: string[] = []
const summaries: SessionSummary[] = []
const featureCounts = {
duplicateSessionIds: [] as string[],
malformedCompleteLines: 0,
incompleteFinalLines: 0,
metadataOnlyAppends: 0,
subagentTranscripts: 0,
taskNotifications: 0,
checkpoints: 0,
windowsDriveMetadata: 0,
uncMetadata: 0,
}
let activityMessages = 0
let activityToolCalls = 0
let activityTokens = 0
const firstSessionId = sessionIdFor(options.seed, 0)
if (options.sessions > 1) {
featureCounts.duplicateSessionIds.push(firstSessionId)
}
for (let sessionIndex = 0; sessionIndex < options.sessions; sessionIndex += 1) {
const projectIndex = projectIndexFor(sessionIndex, options.sessions)
const projectDir = projectDirFor(projectIndex)
const projectPath = join(projectsDir, projectDir)
const isDuplicate = options.sessions > 1 && sessionIndex === options.sessions - 1
const sessionId = isDuplicate
? firstSessionId
: sessionIdFor(options.seed, sessionIndex)
const filePath = join(projectPath, `${sessionId}.jsonl`)
const kind = sessionIndex % PROJECT_COUNT
let workDir = syntheticWorkDir(projectIndex)
if (kind === 5) {
workDir = 'C:\\Synthetic\\Corpus\\Project'
featureCounts.windowsDriveMetadata += 1
} else if (kind === 6) {
workDir = '\\\\synthetic-server\\share\\corpus'
featureCounts.uncMetadata += 1
}
await mkdir(projectPath, { recursive: true })
const user = userRecord(
options.seed,
sessionIndex,
sessionId,
workDir,
sessionIndex % 2 === 1,
)
const userUuid = user.uuid as string
const baseAssistant = assistantRecord(
options.seed,
sessionIndex,
userUuid,
)
const records: JsonRecord[] = [
{
type: 'session-meta',
isMeta: true,
workDir,
timestamp: timestampFor(options.seed, sessionIndex, 0),
},
user,
baseAssistant,
]
for (let entryIndex = records.length; entryIndex < options.entriesPerSession; entryIndex += 1) {
records.push({
type: 'progress',
marker: `synthetic-filler-${sessionIndex}-${entryIndex}`,
timestamp: timestampFor(options.seed, sessionIndex, 20 + entryIndex),
})
}
let messageCount = 2
let modifiedAt = timestampFor(options.seed, sessionIndex, 20)
activityMessages += 2
activityTokens += usageTotal(messageUsage(sessionIndex))
if (kind === 0) {
records[records.length - 1] = {
type: 'session-meta',
isMeta: true,
workDir,
permissionMode: 'default',
timestamp: timestampFor(options.seed, sessionIndex, 55),
}
featureCounts.metadataOnlyAppends += 1
} else if (kind === 1) {
const toolId = `toolu_${seededHex(options.seed, `tool:${sessionIndex}`).slice(0, 12)}`
const toolAssistant = assistantRecord(
options.seed,
sessionIndex,
baseAssistant.uuid as string,
[{
type: 'tool_use',
id: toolId,
name: 'Agent',
input: { description: 'Synthetic subagent benchmark task' },
}],
30,
)
records[3] = toolAssistant
records[4] = {
parentUuid: toolAssistant.uuid,
isSidechain: false,
type: 'user',
message: {
role: 'user',
content: [{
type: 'tool_result',
tool_use_id: toolId,
content: `Synthetic subagent summary\nagentId: synthetic-${sessionIndex}`,
}],
},
uuid: entryId(options.seed, sessionIndex, 'tool-result'),
timestamp: timestampFor(options.seed, sessionIndex, 35),
}
messageCount += 2
modifiedAt = timestampFor(options.seed, sessionIndex, 35)
activityMessages += 2
activityToolCalls += 1
activityTokens += usageTotal(messageUsage(sessionIndex))
const subagentDir = join(projectPath, sessionId, 'subagents')
const subagentPath = join(subagentDir, `agent-synthetic-${sessionIndex}.jsonl`)
await mkdir(subagentDir, { recursive: true })
const subagentUserId = entryId(options.seed, sessionIndex, 'subagent-user')
const subagentRecords = [
{
parentUuid: null,
isSidechain: true,
type: 'user',
message: {
role: 'user',
content: [{ type: 'text', text: 'Synthetic subagent prompt' }],
},
uuid: subagentUserId,
timestamp: timestampFor(options.seed, sessionIndex, 31),
},
{
...assistantRecord(
options.seed,
sessionIndex,
subagentUserId,
[{
type: 'tool_use',
id: `subtool_${sessionIndex}`,
name: 'Read',
input: { file_path: '/synthetic/fixture.ts' },
}],
32,
),
isSidechain: true,
},
]
await writeFile(
subagentPath,
`${subagentRecords.map(record => JSON.stringify(record)).join('\n')}\n`,
)
await utimes(
subagentPath,
new Date(timestampFor(options.seed, sessionIndex, 32)),
new Date(timestampFor(options.seed, sessionIndex, 32)),
)
sourcePaths.push(subagentPath)
featureCounts.subagentTranscripts += 1
activityMessages += subagentRecords.length
activityToolCalls += 1
activityTokens += usageTotal(messageUsage(sessionIndex))
} else if (kind === 2) {
const notificationId = entryId(options.seed, sessionIndex, 'task-notification')
records[3] = {
parentUuid: baseAssistant.uuid,
isSidechain: false,
type: 'user',
message: {
role: 'user',
content: '<task-notification>\n<task-id>synthetic-bg</task-id>\n<tool-use-id>synthetic-tool</tool-use-id>\n<status>completed</status>\n<summary>Synthetic task completed</summary>\n</task-notification>',
},
uuid: notificationId,
timestamp: timestampFor(options.seed, sessionIndex, 30),
}
records[4] = assistantRecord(
options.seed,
sessionIndex,
notificationId,
[{ type: 'text', text: 'Synthetic automatic task response' }],
35,
)
messageCount += 2
modifiedAt = timestampFor(options.seed, sessionIndex, 35)
activityMessages += 2
activityTokens += usageTotal(messageUsage(sessionIndex))
featureCounts.taskNotifications += 1
} else if (kind === 3) {
records[3] = {
type: 'file-history-snapshot',
messageId: entryId(options.seed, sessionIndex, 'checkpoint-message'),
snapshot: {
messageId: userUuid,
trackedFileBackups: {
'src/synthetic.ts': {
backupFileName: `synthetic-${sessionIndex}@v1`,
version: 1,
backupTime: timestampFor(options.seed, sessionIndex, 9),
},
},
timestamp: timestampFor(options.seed, sessionIndex, 9),
},
isSnapshotUpdate: false,
}
featureCounts.checkpoints += 1
}
let contents: string
if (kind === 4) {
const lines = records.map(record => JSON.stringify(record))
lines[lines.length - 2] = '{"type":"synthetic-malformed-complete-line",not-json}'
lines[lines.length - 1] = '{"type":"synthetic-incomplete-final-line","message":'
contents = lines.join('\n')
featureCounts.malformedCompleteLines += 1
featureCounts.incompleteFinalLines += 1
} else {
contents = `${records.map(record => JSON.stringify(record)).join('\n')}\n`
}
await writeFile(filePath, contents)
await utimes(
filePath,
new Date(modifiedAt),
new Date(modifiedAt),
)
transcriptPaths.push(filePath)
sourcePaths.push(filePath)
summaries.push({
sourcePath: sourcePath(configDir, filePath),
sessionId,
title: sessionIndex % 2 === 1
? `Synthetic current-shape prompt ${sessionIndex}`
: `Synthetic old-shape prompt ${sessionIndex}`,
createdAt: timestampFor(options.seed, sessionIndex, 0),
modifiedAt,
messageCount,
})
}
let largeTranscript: {
sourcePath: string
requestedBytes: number
actualBytes: number
} | null = null
if (largeTranscriptBytes > 0) {
const largePath = transcriptPaths.at(-1)!
const actualBytes = await streamLargeTranscript(largePath, largeTranscriptBytes)
largeTranscript = {
sourcePath: sourcePath(configDir, largePath),
requestedBytes: largeTranscriptBytes,
actualBytes,
}
}
summaries.sort((a, b) => {
const timeDiff = Date.parse(b.modifiedAt) - Date.parse(a.modifiedAt)
return timeDiff || a.sourcePath.localeCompare(b.sourcePath)
})
sourcePaths.sort((a, b) => sourcePath(configDir, a).localeCompare(sourcePath(configDir, b)))
const sources: SourceManifestEntry[] = []
for (const filePath of sourcePaths) {
sources.push(await hashSource(configDir, filePath))
}
const manifest = {
corpusVersion: 1,
seed: options.seed,
options: {
sessions: options.sessions,
entriesPerSession: options.entriesPerSession,
largeTranscriptBytes,
seed: options.seed,
},
features: featureCounts,
expected: {
normalizedSessionOrder: summaries.map(
summary => `${summary.sessionId}@${summary.sourcePath}`,
),
totals: {
mainTranscriptFiles: transcriptPaths.length,
sourceFiles: sources.length,
visibleMessages: summaries.reduce(
(total, summary) => total + summary.messageCount,
0,
),
},
summaries,
activityTotals: {
sessions: transcriptPaths.length,
messages: activityMessages,
toolCalls: activityToolCalls,
tokens: activityTokens,
},
},
largeTranscript,
sources,
}
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
return {
configDir,
projectsDir,
transcriptPaths,
manifestPath,
}
}