(options: WarmupRunnerOptions)
| 58 | } |
| 59 | |
| 60 | export async function runWarmup(options: WarmupRunnerOptions): Promise<WarmupResult> { |
| 61 | const { dbPathHash, modelId, embedder, store, stateStore, source, config, checkStop } = options |
| 62 | |
| 63 | let chunksWritten = 0 |
| 64 | try { |
| 65 | const messages = source.readAllMessages() |
| 66 | const total = source.countMessages() |
| 67 | const savedState = stateStore.getState(dbPathHash) |
| 68 | stateStore.updateProgress(dbPathHash, { indexStatus: 'running', totalMessages: total, error: null }) |
| 69 | |
| 70 | // 消息 id -> 流位置,用于进度计数(消息按 ts 排序,id 未必单调) |
| 71 | const streamIndexById = new Map<number, number>() |
| 72 | messages.forEach((m, i) => streamIndexById.set(m.id, i)) |
| 73 | |
| 74 | const resumeMessageId = savedState?.lastIndexedMessageId ?? null |
| 75 | const rawResumeIndex = resumeMessageId !== null ? (streamIndexById.get(resumeMessageId) ?? -1) : -1 |
| 76 | |
| 77 | // Detect non-append-only additions: if the cursor's new stream position doesn't match the |
| 78 | // saved indexed count, older messages were backfilled before the cursor. Clear vectors and |
| 79 | // re-index from scratch to avoid silently missing the backfilled messages. |
| 80 | const isNonAppendOnly = rawResumeIndex >= 0 && rawResumeIndex + 1 !== (savedState?.indexedMessages ?? 0) |
| 81 | if (isNonAppendOnly) { |
| 82 | store.deleteByDbPathHash(dbPathHash) |
| 83 | } |
| 84 | const { chunks, chunkerConfigHash } = chunkMessages({ messages, source: source.getSource(), config }) |
| 85 | |
| 86 | const chunkRanges = chunks.map((chunk) => ({ |
| 87 | chunk, |
| 88 | startIndex: streamIndexById.get(chunk.startMessageId) ?? -1, |
| 89 | endIndex: streamIndexById.get(chunk.endMessageId) ?? -1, |
| 90 | })) |
| 91 | |
| 92 | let resumeIndex = isNonAppendOnly ? -1 : rawResumeIndex |
| 93 | const hasAppendedMessages = !isNonAppendOnly && rawResumeIndex >= 0 && total > (savedState?.totalMessages ?? 0) |
| 94 | if (hasAppendedMessages) { |
| 95 | const cursorRange = |
| 96 | chunkRanges.find((range) => range.startIndex <= rawResumeIndex && rawResumeIndex <= range.endIndex) ?? |
| 97 | chunkRanges.filter((range) => range.endIndex <= rawResumeIndex).sort((a, b) => b.endIndex - a.endIndex)[0] |
| 98 | |
| 99 | if (cursorRange) { |
| 100 | const parentRanges = chunkRanges.filter((range) => range.chunk.parentId === cursorRange.chunk.parentId) |
| 101 | const rewindIndex = Math.min(...parentRanges.map((range) => range.startIndex).filter((index) => index >= 0)) |
| 102 | const rewindMessage = messages[rewindIndex] |
| 103 | if (rewindMessage) { |
| 104 | store.deleteByModelFromPosition({ |
| 105 | dbPathHash, |
| 106 | modelId, |
| 107 | startTs: rewindMessage.ts, |
| 108 | startMessageId: rewindMessage.id, |
| 109 | }) |
| 110 | resumeIndex = rewindIndex - 1 |
| 111 | } |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | // 续跑时统计已写入 chunk 数,保证 chunkCount 连续 |
| 116 | let storedChunkCount = store.countChunks(dbPathHash, modelId) |
| 117 | if (resumeIndex < 0) storedChunkCount = 0 |
no test coverage detected