feat: P4 qdrant collection lifecycle (self-heal + guarded rebuild)
- shared TS collection manager: self-heal creates missing collection (1024/cosine) and missing keyword payload indexes; never mutates incompatible contracts (semantic_index_incompatible); async index visibility polled with bounded deadline - session admission (qdrantEnsure) uses the manager in self-heal mode; operator path keeps require_existing semantics - runtime lease exposes semanticQdrantUrl to the operator - operator commands vector-inspect/vector-rebuild with exact confirmation guards - thothctl workspace vector inspect|rebuild (Go) with --collection/--confirm/--destroy - p4 acceptance runner: real Qdrant (v1.18.2) lifecycle checks, 11/11 PASS - docs: CLI contract, manual walkthrough P4 (PENDING), PROJECT_STATE
This commit is contained in:
@@ -10,6 +10,7 @@ import { clearPrincipalEnvironment, principalEnvironment, type PrincipalContext
|
||||
import { secretValue, type SecretBundleConfig } from "../config/secret-bundle.js";
|
||||
import { renderWorkspaceRuntimeFromSnapshotPath } from "../workspaces/runtime-config-lease.js";
|
||||
import {
|
||||
DEFAULT_SEMANTIC_RUNTIME,
|
||||
type RuntimeInstallationOverlay,
|
||||
type RuntimePaths,
|
||||
type SemanticRuntimeConfig,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
validateOperationalWorkspace,
|
||||
type WorkspaceDescriptor,
|
||||
} from "../workspaces/schema.js";
|
||||
import { reconcileCollection } from "../workspaces/qdrant-collection.js";
|
||||
|
||||
export interface ThtConfig extends SecretBundleConfig {
|
||||
thtBin: string;
|
||||
@@ -29,6 +31,8 @@ export interface ThtConfig extends SecretBundleConfig {
|
||||
secretRoots?: readonly string[];
|
||||
semanticRuntime: SemanticRuntimeConfig;
|
||||
qdrantRequest?: typeof fetch;
|
||||
/** "self_heal" for session admission (create missing collections/indexes), default "require_existing". */
|
||||
qdrantCollectionMode?: "self_heal" | "require_existing";
|
||||
}
|
||||
|
||||
export interface RuntimeConfigLease {
|
||||
@@ -216,7 +220,7 @@ export class ThtRunner {
|
||||
throw new Error("registry workspace runtime requires an absolute data root");
|
||||
})(),
|
||||
secretRoots: this.cfg.secretRoots ?? [],
|
||||
semanticRuntime: this.cfg.semanticRuntime,
|
||||
semanticRuntime: this.cfg.semanticRuntime ?? DEFAULT_SEMANTIC_RUNTIME,
|
||||
});
|
||||
const path = this.createRuntimeSnapshot(rendered.renderedConfig);
|
||||
let released = false;
|
||||
@@ -561,6 +565,7 @@ export class ThtRunner {
|
||||
async qdrantEnsure(
|
||||
workspace: WorkspaceDescriptor,
|
||||
timeoutSec: number,
|
||||
mode: "self_heal" | "require_existing" = "require_existing",
|
||||
): Promise<QdrantEnsureResult> {
|
||||
let descriptor;
|
||||
try {
|
||||
@@ -572,32 +577,16 @@ export class ThtRunner {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), Math.max(1, timeoutSec) * 1000);
|
||||
try {
|
||||
const url = new URL(
|
||||
`/collections/${encodeURIComponent(collection.collection)}`,
|
||||
this.cfg.semanticRuntime.internalQdrantUrl,
|
||||
);
|
||||
const request = this.cfg.qdrantRequest ?? fetch;
|
||||
const response = await request(url.toString(), { method: "GET", signal: controller.signal });
|
||||
if (response.status === 404) {
|
||||
return { ok: false, code: "semantic_index_incompatible" };
|
||||
}
|
||||
if (!response.ok) return { ok: false, code: "workspace_not_activatable" };
|
||||
const body = await response.json() as any;
|
||||
const result = body?.result;
|
||||
const vectors = result?.config?.params?.vectors;
|
||||
const payloadSchema = result?.payload_schema;
|
||||
const configurationMatches = vectors
|
||||
&& vectors.size === collection.dimensions
|
||||
&& typeof vectors.distance === "string"
|
||||
&& vectors.distance.toLowerCase() === collection.distance;
|
||||
const indexesMatch = payloadSchema
|
||||
&& typeof payloadSchema === "object"
|
||||
&& REQUIRED_QDRANT_PAYLOAD_INDEXES.every(
|
||||
(field) => payloadSchema[field]?.data_type === "keyword",
|
||||
);
|
||||
return configurationMatches && indexesMatch
|
||||
? { ok: true }
|
||||
: { ok: false, code: "semantic_index_incompatible" };
|
||||
const checked = await reconcileCollection({
|
||||
baseUrl: this.cfg.semanticRuntime.internalQdrantUrl,
|
||||
collection: collection.collection,
|
||||
dimensions: collection.dimensions,
|
||||
distance: collection.distance,
|
||||
mode,
|
||||
request: this.cfg.qdrantRequest ?? fetch,
|
||||
signal: controller.signal,
|
||||
});
|
||||
return checked;
|
||||
} catch {
|
||||
return { ok: false, code: "workspace_not_activatable" };
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user