fix(rag): close audit gaps in the in-house engine

An adversarial audit of the piragi -> in-house swap surfaced nine confirmed
issues; this fixes all of them.

- Re-ingest now REPLACES a source's chunks instead of appending. Add
  VectorStore.delete_by_source and BaseIndexPlugin.replace_on_reingest (default
  True), called before add_chunks in both ingest paths. Without it every
  startup / periodic / manual reindex appended a fresh copy of each doc's
  chunks, growing the tables unbounded and crowding out distinct results.
  Conversations opt OUT (replace_on_reingest=False): their many messages share
  one source URI, so delete-by-source would wipe history.
- index_* now honor the plugin IngestResult. The explicit record endpoints
  (error / standard / decision / review / learning) raise on failure instead of
  writing a green tracking row for content that never persisted;
  conversation / journal indexing stays best-effort but skips the tracking row
  when the embed fails. index_message / index_entry return IngestResult.
- A deprecated index type (code) now returns 404 instead of a 500 leaked from
  _get_plugin's missing-plugin error: add OptimalService.is_index_registered
  and guard the stats / clear / refresh routes. The panel drops the dead 'Code'
  category, filter, badge, label, and mock data.
- Panel: getContext reads 'results' (matches SearchResponse) instead of a
  non-existent 'context' field; the reindex toast no longer reports phantom
  '0 code files'; the stats 'Updated' label uses the max timestamp across
  indexes rather than indexes[0]; ProactiveContextItem matches the wire shape.
- Drop the always-zero per-document chunk_count from the documents API.
- Remove dead RAG settings (hybrid_search, cross_encoder) the engine never
  consumed, and correct stale piragi / BM25 references in code, README, and
  CLAUDE.md. Delete the unused duplicate roboco/kb embedder package the swap
  shipped.

Adds tests for replace-on-reingest (incl. the conversations carve-out) and the
deprecated-index 404.
This commit is contained in:
Renn F
2026-06-15 04:55:16 +02:00
parent e53eb5b7ee
commit 6422f77bb9
24 changed files with 251 additions and 1134 deletions
@@ -3,11 +3,10 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { KBStats, KBIndexType } from "@/types";
import { Database, Code, FileText, MessageSquare, BookOpen, AlertTriangle, Scale, GitBranch, ClipboardCheck, Lightbulb } from "lucide-react";
import { Database, FileText, MessageSquare, BookOpen, AlertTriangle, Scale, GitBranch, ClipboardCheck, Lightbulb } from "lucide-react";
import { formatDistanceToNow } from "date-fns";
const indexIcons: Record<KBIndexType, React.ReactNode> = {
[KBIndexType.CODE]: <Code className="h-4 w-4 text-purple-500" />,
[KBIndexType.DOCUMENTATION]: <FileText className="h-4 w-4 text-blue-500" />,
[KBIndexType.CONVERSATIONS]: <MessageSquare className="h-4 w-4 text-green-500" />,
[KBIndexType.JOURNALS]: <BookOpen className="h-4 w-4 text-orange-500" />,
@@ -19,7 +18,6 @@ const indexIcons: Record<KBIndexType, React.ReactNode> = {
};
const indexLabels: Record<KBIndexType, string> = {
[KBIndexType.CODE]: "Code",
[KBIndexType.DOCUMENTATION]: "Docs",
[KBIndexType.CONVERSATIONS]: "Convos",
[KBIndexType.JOURNALS]: "Journals",
@@ -73,6 +71,12 @@ export function KBStatsCard({ stats, isLoading }: KBStatsCardProps) {
);
}
const latestUpdated = stats.indexes.reduce<string | null>(
(acc, idx) =>
idx.last_updated && (!acc || idx.last_updated > acc) ? idx.last_updated : acc,
null,
);
return (
<Card>
<CardHeader className="pb-2">
@@ -104,9 +108,9 @@ export function KBStatsCard({ stats, isLoading }: KBStatsCardProps) {
<span>{stats.total_chunks.toLocaleString()}</span>
</div>
</div>
{stats.indexes[0]?.last_updated && (
{latestUpdated && (
<p className="text-xs text-muted-foreground pt-1">
Updated {formatDistanceToNow(new Date(stats.indexes[0].last_updated))} ago
Updated {formatDistanceToNow(new Date(latestUpdated))} ago
</p>
)}
</CardContent>