Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions docs/reindex-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,38 @@

Use this sequence when applying RAG indexing changes to the live `Clinical KB Database` Supabase project.

## Safe sequence
## Consolidated pipeline command

The quickest way to run a full reindex cycle is the consolidated pipeline command:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Queue documents before calling this a full reindex

In the common release case where all documents are already indexed and there are no open ingestion_jobs, scripts/reindex.ts hits its queue-clear check and exits successfully without enqueueing any document for the new indexing code. Since this runbook is specifically for applying RAG indexing changes, describing npm run reindex as a full reindex cycle can leave the corpus on the old chunks/enrichment while operators proceed to evals; either add an enqueue step/command or label this as draining an already queued reindex.

Useful? React with 👍 / 👎.


```sh
npm run reindex
```

This single command encapsulates the full safe sequence below: it confirms the target project, checks Supabase health, snapshots reindex health, applies ingestion queue recovery (with an interactive confirmation prompt), runs the worker, and repeats until the queue is clear or the round limit is reached.

Options:

| Flag | Default | Description |
|---|---|---|
| `--yes` | off | Skip confirmation prompts (non-interactive / CI use) |
| `--max-rounds` | 10 | Maximum worker iterations before stopping |
| `--limit` | 20 | Recovery action limit per round |

```sh
# Non-interactive, up to 5 worker rounds:
npm run reindex -- --yes --max-rounds 5
```

## Manual safe sequence

If you prefer to run each step by hand (or need to apply a migration in between):

1. Confirm the target project is `sjrfecxgysukkwxsowpy`.
2. Run `npm run supabase:recovery-status`.
3. Apply any pending local migration only after the probe succeeds.
4. Run `npm run reindex:health`.
5. If stale or failed jobs exist, run `npm run recover:ingestion -- --apply --limit 20`.
5. If stale or failed jobs exist, run `npm run recover:ingestion` and confirm the prompt.
6. Run `npm run worker:once` with the conservative defaults from `.env.example`.
Comment on lines 34 to 37
7. Repeat `npm run reindex:health` and `npm run worker:once` until the queue is clear.
8. Run indexing and RAG evals only after documents have adaptive chunks and retrieval synopses.
Expand Down
4 changes: 4 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const eslintConfig = defineConfig([
".tmp-visual/**",
"sample-documents/**",
"scratch/**",
".claude/**",
"**/.claude/**",
".tmp-visual/**",
"**/.tmp-visual/**",
Comment on lines +21 to +24
"next-env.d.ts",
]),
]);
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"check:supabase-project": "tsx scripts/check-supabase-project.ts",
"check:indexing": "tsx scripts/check-indexing.ts",
"recover:ingestion": "tsx scripts/recover-ingestion-queue.ts",
"reindex": "tsx scripts/reindex.ts",
"reindex:health": "tsx scripts/reindex-health.ts",
"supabase:recovery-status": "tsx scripts/supabase-recovery-status.ts",
"promote:query-misses": "tsx scripts/promote-query-misses.ts",
Expand Down
20 changes: 20 additions & 0 deletions scripts/cli-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createInterface } from "node:readline";

/**
* Prompts the user with a yes/no question and returns their answer.
* Returns `false` if stdin is not a TTY (e.g. when piped).
*/
export function confirm(question: string): Promise<boolean> {
if (!process.stdin.isTTY) {
console.log(" Non-interactive input detected; defaulting to No.");
return Promise.resolve(false);
}

return new Promise((resolve) => {
const rl = createInterface({ input: process.stdin, output: process.stdout });
rl.question(`${question} (y/N) `, (answer: string) => {
Comment thread
BigSimmo marked this conversation as resolved.
rl.close();
resolve(answer.trim().toLowerCase() === "y");
});
Comment on lines +15 to +18
});
}
70 changes: 51 additions & 19 deletions scripts/recover-ingestion-queue.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { loadEnvConfig } from "@next/env";
import type { IngestionRecoveryJob } from "@/lib/ingestion-recovery";
import { confirm } from "./cli-utils";

loadEnvConfig(process.cwd());

Expand All @@ -9,6 +10,14 @@ type RecoveryDocument = {
chunk_count?: number | null;
};

type RawJobRow = {
id: string;
document_id: string;
status: string | null;
locked_at: string | null;
documents: RecoveryDocument | RecoveryDocument[] | null;
};

function supabaseStageError(stage: string, error: unknown) {
const record = error && typeof error === "object" ? (error as Record<string, unknown>) : {};
const message =
Expand All @@ -35,6 +44,7 @@ function parseArgs(argv: string[]) {
};
return {
apply: argv.includes("--apply"),
yes: argv.includes("--yes"),
staleAfterMinutes: Number.parseInt(valueFor("stale-after-minutes") ?? "", 10),
limit: Number.parseInt(valueFor("limit") ?? "", 10),
};
Expand All @@ -59,7 +69,12 @@ async function main() {
: env.WORKER_STALE_AFTER_MINUTES;
const limit = Number.isFinite(args.limit) ? args.limit : 20;
const supabase = createAdminClient();

console.log("=== Ingestion Queue Recovery ===");
console.log(`Checking Supabase health...`);
assertSupabaseHealthy(await probeSupabaseHealth(supabase), "Ingestion queue recovery");
console.log(" Supabase is healthy.\n");

const { data, error } = await supabase
.from("ingestion_jobs")
.select("id,document_id,status,locked_at,documents(status,page_count,chunk_count)")
Expand All @@ -68,7 +83,7 @@ async function main() {

if (error) throw supabaseStageError("load open ingestion jobs", error);

const jobs = (data ?? []).map((job) => ({
const jobs = (data ?? []).map((job: RawJobRow) => ({
...job,
documents: Array.isArray(job.documents) ? (job.documents[0] as RecoveryDocument | undefined) : job.documents,
})) as IngestionRecoveryJob[];
Expand All @@ -77,29 +92,43 @@ async function main() {
const resetDocumentIds = Array.from(
new Set(actions.filter((action) => action.action === "retry").map((action) => action.documentId)),
);
const supersedeCount = actions.filter((action) => action.action === "supersede").length;
const retryCount = actions.filter((action) => action.action === "retry").length;
const remainingCount = Math.max(0, plan.actions.length - actions.length);

console.log(
JSON.stringify(
{
mode: args.apply ? "apply" : "dry-run",
staleAfterMinutes,
limit,
scannedJobs: jobs.length,
resetDocuments: resetDocumentIds.length,
supersedeJobs: actions.filter((action) => action.action === "supersede").length,
retryJobs: actions.filter((action) => action.action === "retry").length,
remainingActions: Math.max(0, plan.actions.length - actions.length),
},
null,
2,
),
);
console.log(`Stale-after threshold : ${staleAfterMinutes} min`);
console.log(`Action limit : ${limit}`);
console.log(`Scanned jobs : ${jobs.length}`);
console.log(`Documents to reset : ${resetDocumentIds.length}`);
console.log(`Jobs to supersede : ${supersedeCount}`);
console.log(`Jobs to retry : ${retryCount}`);
if (remainingCount > 0) {
console.log(`Remaining (over limit): ${remainingCount}`);
}

if (!args.apply) {
console.log("Dry run only. Re-run with --apply to mutate the ingestion queue.");
if (actions.length === 0) {
console.log("\nNothing to recover. Queue looks healthy.");
return;
}

let shouldApply = args.apply;

if (!shouldApply) {
if (args.yes) {
shouldApply = true;
} else {
console.log("");
shouldApply = await confirm("Apply these changes?");
}
}

if (!shouldApply) {
console.log("\nNo changes applied. Re-run with --apply or confirm interactively to mutate the ingestion queue.");
return;
}

console.log("\nApplying recovery...");

for (const documentId of resetDocumentIds) {
const { error: resetError } = await supabase.rpc("reset_document_index", { p_document_id: documentId });
if (resetError) throw supabaseStageError("reset document index", resetError);
Expand Down Expand Up @@ -146,6 +175,9 @@ async function main() {
}

console.log("Ingestion queue recovery applied.");
if (remainingCount > 0) {
console.log(`\n${remainingCount} action(s) remain over the limit. Re-run to process the next batch.`);
}
}

main().catch((error) => {
Expand Down
Loading