fix(sync,rag): keep restored rows, reconcile after tombstone pruning, reject partial knowledge docs, surface RAG indexing failures

This commit is contained in:
Yun Chan 2026-09-28 02:16:14 +09:00
parent bbaf1e0a99
commit 3a46437f28
13 changed files with 1630 additions and 448 deletions

View file

@ -955,6 +955,30 @@ async function pushKnowledgeDocument(ctx: PushContext, id: string): Promise<Sync
return null
}
/**
* 서버 청크가 문서 행의 chunk_count만큼 빠짐없이(chunk_index 0..n-1) 올라와 있으면 원문 배열을,
* 아직 업로드 중이거나 업로드가 중간에 실패했거나 조회가 잘렸으면 null을 돌려준다.
* chunk_count를 모르는 행(옛 클라이언트)은 0부터 이어지는지만 본다.
*/
export function completeKnowledgeChunks(chunkRows: readonly RemoteRow[], expectedCount: number | null): string[] | null {
if (chunkRows.length === 0) return null
if (expectedCount !== null && chunkRows.length !== expectedCount) return null
const byIndex = new Map<number, string>()
for (const c of chunkRows) {
const index = num(c.chunk_index)
const content = str(c.content)
if (index === null || content === null || byIndex.has(index)) return null
byIndex.set(index, content)
}
const chunks: string[] = []
for (let i = 0; i < byIndex.size; i++) {
const content = byIndex.get(i)
if (content === undefined) return null
chunks.push(content)
}
return chunks
}
const knowledgeAdapter: SyncAdapter = {
entity: 'knowledge_documents',
pull: { columns: 'id,title,file_name,file_type,chunk_count,created_at,updated_at' },
@ -982,9 +1006,10 @@ const knowledgeAdapter: SyncAdapter = {
const chunkRows = await ctx.remote.selectChildren('knowledge_chunks', 'document_id', id, 'chunk_index,content', 'chunk_index')
// 네트워크를 기다리는 사이 로그아웃·계정 전환이 있었으면 다른 사용자 DB에 쓰지 않는다.
assertCurrent(ctx)
const chunks = chunkRows.map((c) => str(c.content)).filter((c): c is string => c !== null)
// 다른 기기가 문서 행을 올리고 청크를 아직 못 올렸을 수 있다 — 다음 pull에서 다시 본다.
if (chunks.length === 0) return 'deferred'
// 다른 기기가 문서 행을 올리고 청크를 아직(또는 일부만) 올렸을 수 있다 — 다음 pull에서 다시 본다.
// 일부만 받아 저장하면 이후 pull은 "이미 있음"으로 건너뛰어 잘린 문서가 영구히 남는다.
const chunks = completeKnowledgeChunks(chunkRows, num(row.chunk_count))
if (chunks === null) return 'deferred'
return getRAGService().applyRemoteDocument({
id,
fileName: str(row.file_name) ?? str(row.title) ?? 'document',