fix: upsert in bounded chunks, recreate Qdrant indexes on rebuild, larger maintenance tmpfs

This commit is contained in:
2026-08-13 19:02:52 +02:00
parent 84b233d937
commit f31b1e61ac
4 changed files with 56 additions and 12 deletions
@@ -11,6 +11,7 @@ import {
} from "./preprocessing-state.js";
import type { DeterministicRuntimeConfigLease } from "./runtime-config-lease.js";
import { readAnnotationsSync } from "./annotations-sync.js";
import { reconcileCollection } from "./qdrant-collection.js";
export interface WorkspaceOperationResult {
schemaVersion: 1;
@@ -150,12 +151,15 @@ export class WorkspacePreprocessingService {
const q = `${runtime.configLease.semanticQdrantUrl}/collections/${encodeURIComponent(collection)}`;
const del = await fetch(q, { method: "DELETE" });
if (!del.ok && del.status !== 404) return baseResult(runtime, "vector rebuild", "failed", "semantic_index_incompatible", { warnings: ["collection delete failed"] });
const put = await fetch(q, {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ vectors: { size: runtime.workspace.semantic_index.vector_store.dimensions, distance: runtime.workspace.semantic_index.vector_store.distance } }),
// Recreate the complete contract (dimensions + distance + the 8 required keyword indexes).
const recreated = await reconcileCollection({
baseUrl: runtime.configLease.semanticQdrantUrl,
collection,
dimensions: runtime.workspace.semantic_index.vector_store.dimensions,
distance: runtime.workspace.semantic_index.vector_store.distance,
mode: "self_heal",
});
if (!put.ok) return baseResult(runtime, "vector rebuild", "failed", "semantic_index_incompatible", { warnings: ["collection recreate failed"] });
if (!recreated.ok) return baseResult(runtime, "vector rebuild", "failed", "semantic_index_incompatible", { warnings: ["collection recreate failed"] });
return baseResult(runtime, "vector rebuild", "succeeded", "ok", { warnings: [`recreated collection=${collection}`] });
}
async inspect(options: { workspaceId: string }): Promise<WorkspaceOperationResult> {
@@ -123,6 +123,7 @@ function runtime(workspace = baseWorkspace, workspaceId = workspace.workspace.id
catalogBlob: "c".repeat(40),
configDigest: "sha256:config",
bindingDigest: "sha256:bindings",
semanticQdrantUrl: "http://qdrant:6333",
effectiveConfig: {
schemaVersion: 1,
dwh: {
@@ -491,3 +492,36 @@ test("full runs continue after schema accept only when the accepted blob matches
const stillBlocked = await second.service.run({ workspaceId: "psd-clinical", resumeRunId: secondRunId });
expect(stillBlocked).toMatchObject({ status: "blocked", code: "manual_review_required" });
});
test("vector rebuild recreates the full collection contract including keyword indexes", async () => {
const f = fixture();
// mock fetch: DELETE ok, then reconcileCollection self-heals create + indexes (real fetch in deps)
const calls: string[] = [];
const fakeFetch = async (url: string, init?: any) => {
calls.push(`${init?.method ?? "GET"} ${url}`);
if ((init?.method ?? "GET") === "DELETE") return new Response("", { status: 200 });
if (url.endsWith("/collections/psd-clinical") && init?.method === "PUT") return new Response("", { status: 200 });
if (url.endsWith("/collections/psd-clinical") && init?.method === "GET") {
return new Response(JSON.stringify({ result: { config: { params: { vectors: { size: 1024, distance: "Cosine" } } }, payload_schema: { content_hash: { data_type: "keyword" }, document_id: { data_type: "keyword" }, kind: { data_type: "keyword" }, record_key: { data_type: "keyword" }, record_kind: { data_type: "keyword" }, vector_generation: { data_type: "keyword" }, workspace_id: { data_type: "keyword" }, workspace_revision: { data_type: "keyword" } } } }), { status: 200 });
}
if (url.endsWith("/collections/psd-clinical/index") && init?.method === "PUT") return new Response("", { status: 200 });
return new Response(JSON.stringify({ result: {} }), { status: 200 });
};
const service = new WorkspacePreprocessingService({
dataRoot: f.dataRoot,
acquireActiveRuntime: async () => runtime(baseWorkspace),
runChild: vi.fn(),
listSessions: async () => [],
semanticPreflight: async () => ({ ok: true }),
});
// replace global fetch used by vectorRebuild/reconcileCollection
const original = globalThis.fetch;
globalThis.fetch = fakeFetch as any;
try {
const result = await service.vectorRebuild({ workspaceId: "psd-clinical", collection: "psd-clinical", confirm: "psd-clinical", destroy: true });
expect(result).toMatchObject({ status: "succeeded", code: "ok" });
} finally {
globalThis.fetch = original;
}
expect(calls.some((c) => c.startsWith("DELETE "))).toBe(true);
});