Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function AddToBrain({
const [executing, setExecuting] = useState(false);
const [executeError, setExecuteError] = useState<string | null>(null);

const fetchJobDetail = useCallback(async (jobId: number) => {
const fetchJobDetail = useCallback(async (jobId: string) => {
setLoadingDetail(true);
try {
const res = await fetch(`/api/ingest/${jobId}`);
Expand Down
2 changes: 1 addition & 1 deletion dashboards/open-brain-dashboard-next/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export async function triggerIngest(
apiKey: string,
text: string,
opts?: { dry_run?: boolean }
): Promise<{ job_id: number; status: string }> {
): Promise<{ job_id: string; status: string }> {
return apiFetch(apiKey, "/ingest", {
method: "POST",
body: JSON.stringify({ text, ...opts }),
Expand Down
8 changes: 4 additions & 4 deletions dashboards/open-brain-dashboard-next/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export interface Reflection {
}

export interface IngestionJob {
id: number;
id: string;
source_label: string;
status: string;
extracted_count: number;
Expand Down Expand Up @@ -142,8 +142,8 @@ export interface ReflectionInput {
}

export interface IngestionItem {
id: number | string;
job_id: number;
id: string;
job_id: string;
content: string;
type: string;
fingerprint: string;
Expand All @@ -164,7 +164,7 @@ export type AddToBrainMode = "auto" | "single" | "extract";
export interface AddToBrainResult {
path: "single" | "extract";
thought_id?: string;
job_id?: number;
job_id?: string;
type?: string;
status?: string;
extracted_count?: number | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ export async function POST(

const { id } = await params;

// WR-04 / BL-03: Validate id is a positive integer before forwarding
const idNum = Number(id);
if (!Number.isInteger(idNum) || idNum <= 0) {
// WR-04 / BL-03: Validate id is a UUID before forwarding. OB1 ingestion job
// ids are UUIDs, not integers — the old positive-integer check rejected them.
const UUID_RE =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!UUID_RE.test(id)) {
return NextResponse.json({ error: "Invalid id" }, { status: 400 });
}

Expand All @@ -34,7 +36,7 @@ export async function POST(
// BL-03: Re-verify the session owns this job by fetching it first.
// The REST gateway filters by the session's x-brain-key, so a 404/403 here
// indicates the job is not visible to the caller.
const verifyRes = await fetch(`${API_URL}/ingestion-jobs/${idNum}`, {
const verifyRes = await fetch(`${API_URL}/ingestion-jobs/${id}`, {
headers: { "x-brain-key": apiKey, "Content-Type": "application/json" },
});
if (!verifyRes.ok) {
Expand All @@ -43,7 +45,7 @@ export async function POST(
// of a misleading "denied" message.
if (verifyRes.status >= 500) {
console.error(
`[ingest/[id]/execute] preflight upstream 5xx for job ${idNum}:`,
`[ingest/[id]/execute] preflight upstream 5xx for job ${id}:`,
verifyRes.status
);
return NextResponse.json(
Expand All @@ -62,7 +64,7 @@ export async function POST(
);
}

const res = await fetch(`${API_URL}/ingestion-jobs/${idNum}/execute`, {
const res = await fetch(`${API_URL}/ingestion-jobs/${id}/execute`, {
method: "POST",
headers: { "x-brain-key": apiKey, "Content-Type": "application/json" },
});
Expand Down
18 changes: 10 additions & 8 deletions dashboards/open-brain-dashboard-pro/app/api/ingest/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ function normalizeItem(raw: Record<string, unknown>): IngestionItem {
};

return {
id: raw.id as number,
job_id: raw.job_id as number,
id: raw.id as string,
job_id: raw.job_id as string,
content: (raw.extracted_content ?? raw.content ?? "") as string,
action: (raw.action ?? "skip") as string,
reason: (raw.reason as string) ?? null,
status: (raw.status ?? "pending") as string,
matched_thought_id: (raw.matched_thought_id as number) ?? null,
matched_thought_id: (raw.matched_thought_id as string) ?? null,
similarity_score: raw.similarity_score != null ? Number(raw.similarity_score) : null,
result_thought_id: (raw.result_thought_id as number) ?? null,
result_thought_id: (raw.result_thought_id as string) ?? null,
meta: parsedMeta,
};
}
Expand All @@ -41,9 +41,11 @@ export async function GET(

const { id } = await params;

// WR-04: Validate id is a positive integer before forwarding
const idNum = Number(id);
if (!Number.isInteger(idNum) || idNum <= 0) {
// WR-04: Validate id is a UUID before forwarding. OB1 ingestion job ids are
// UUIDs, not integers — the old positive-integer check rejected them.
const UUID_RE =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!UUID_RE.test(id)) {
return NextResponse.json({ error: "Invalid id" }, { status: 400 });
}

Expand All @@ -56,7 +58,7 @@ export async function GET(
}

try {
const res = await fetch(`${API_URL}/ingestion-jobs/${idNum}`, {
const res = await fetch(`${API_URL}/ingestion-jobs/${id}`, {
headers: { "x-brain-key": apiKey, "Content-Type": "application/json" },
});
const data = await res.json();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export function AddToBrain({
const [executing, setExecuting] = useState(false);
const [executeError, setExecuteError] = useState<string | null>(null);

const fetchJobDetail = useCallback(async (jobId: number) => {
const fetchJobDetail = useCallback(async (jobId: string) => {
setLoadingDetail(true);
try {
const res = await fetch(`/api/ingest/${jobId}`);
Expand Down
2 changes: 1 addition & 1 deletion dashboards/open-brain-dashboard-pro/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ export async function triggerIngest(
apiKey: string,
text: string,
opts?: { dry_run?: boolean; skip_classification?: boolean }
): Promise<{ job_id: number; status: string }> {
): Promise<{ job_id: string; status: string }> {
return apiFetch(apiKey, "/ingest", {
method: "POST",
body: JSON.stringify({ text, ...opts }),
Expand Down
12 changes: 6 additions & 6 deletions dashboards/open-brain-dashboard-pro/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export interface Thought {
}

export interface IngestionJob {
id: number;
id: string;
source_label: string;
status: string;
extracted_count: number;
Expand Down Expand Up @@ -69,16 +69,16 @@ export interface IngestionItemMeta {
}

export interface IngestionItem {
id: number;
job_id: number;
id: string;
job_id: string;
/** The extracted thought content (DB column: extracted_content) */
content: string;
action: string; // add, skip, create_revision, append_evidence
reason: string | null;
status: string;
matched_thought_id: number | null;
matched_thought_id: string | null;
similarity_score: number | null;
result_thought_id: number | null;
result_thought_id: string | null;
/** Parsed metadata — type, importance, tags, source_snippet */
meta: IngestionItemMeta;
}
Expand All @@ -93,7 +93,7 @@ export type AddToBrainMode = "auto" | "single" | "extract";
export interface AddToBrainResult {
path: "single" | "extract";
thought_id?: number;
job_id?: number;
job_id?: string;
type?: string;
status?: string;
extracted_count?: number | null;
Expand Down
6 changes: 3 additions & 3 deletions integrations/smart-ingest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ You should get a response showing extracted thoughts and their reconciliation ac
```json
{
"status": "dry_run_complete",
"job_id": 1,
"job_id": "123e4567-e89b-12d3-a456-426614174000",
"extracted_count": 3,
"added_count": 3,
"skipped_count": 0,
Expand All @@ -189,7 +189,7 @@ Once you're satisfied with the dry-run results, commit them to the database:
curl -X POST "https://<your-project-ref>.supabase.co/functions/v1/smart-ingest/execute" \
-H "Content-Type: application/json" \
-H "x-brain-key: your-access-key" \
-d '{ "job_id": 1 }'
-d '{ "job_id": "123e4567-e89b-12d3-a456-426614174000" }'
```

### 5. Verify Results
Expand Down Expand Up @@ -237,7 +237,7 @@ Execute a previously dry-run job.

| Parameter | Type | Description |
|-----------|------|-------------|
| `job_id` | number | ID of the dry-run job to execute |
| `job_id` | string (UUID) | ID of the dry-run job to execute |
| `skip_classification` | boolean | Override classification behavior for this execution |

## How It Connects to Other Components
Expand Down
59 changes: 33 additions & 26 deletions integrations/smart-ingest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,14 +141,14 @@ interface IngestionItem {
content_fingerprint: string;
action: ReconcileAction;
reason: string;
matched_thought_id: number | null;
matched_thought_id: string | null;
similarity_score: number | null;
status: "pending" | "executed" | "failed";
error_message: string | null;
}

interface IngestionJob {
id?: number;
id?: string;
input_hash: string;
source_label: string | null;
source_type: string | null;
Expand All @@ -164,8 +164,8 @@ interface IngestionJob {
}

type UpsertThoughtResult = {
thought_id?: number;
id?: number;
thought_id?: string;
id?: string;
};

// ── Auth ────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -347,15 +347,17 @@ function mergeTags(existing: unknown, extras: string[]): string[] {
]);
}

function extractThoughtId(value: unknown): number | null {
if (typeof value === "number" && Number.isFinite(value)) return value;
function extractThoughtId(value: unknown): string | null {
// OB1 thought ids are UUIDs (strings). A successful upsert_thought may return
// the id as a bare string, or wrapped as { thought_id } / { id }.
if (typeof value === "string" && value.trim().length > 0) return value;
if (value && typeof value === "object" && "thought_id" in value) {
const thoughtId = (value as UpsertThoughtResult).thought_id;
if (typeof thoughtId === "number" && Number.isFinite(thoughtId)) return thoughtId;
if (typeof thoughtId === "string" && thoughtId.trim().length > 0) return thoughtId;
}
if (value && typeof value === "object" && "id" in value) {
const id = (value as UpsertThoughtResult).id;
if (typeof id === "number" && Number.isFinite(id)) return id;
if (typeof id === "string" && id.trim().length > 0) return id;
}
return null;
}
Expand Down Expand Up @@ -595,7 +597,7 @@ async function reconcileThought(
tags: thought.tags,
source_snippet: thought.source_snippet,
content_fingerprint: fingerprint,
matched_thought_id: null as number | null,
matched_thought_id: null as string | null,
similarity_score: null as number | null,
};

Expand Down Expand Up @@ -649,7 +651,7 @@ async function reconcileThought(

const topMatch = matches[0];
const similarity = topMatch.similarity as number;
const matchedId = topMatch.id as number;
const matchedId = topMatch.id as string;
const existingContent = (topMatch.content ?? "") as string;

base.matched_thought_id = matchedId;
Expand Down Expand Up @@ -679,7 +681,7 @@ async function executeItem(
sourceType: string | null,
sourceMetadata?: Record<string, unknown> | null,
skipClassification = false,
): Promise<number | null> {
): Promise<string | null> {
switch (item.action) {
case "add": {
const prepared = await prepareThoughtPayload(item.content, {
Expand Down Expand Up @@ -822,7 +824,7 @@ async function createJob(
job: IngestionJob,
sourceMetadata?: Record<string, unknown> | null,
inputLength: number = 0,
): Promise<number> {
): Promise<string> {
const { data, error } = await supabase.from("ingestion_jobs").insert({
input_hash: job.input_hash,
source_label: job.source_label,
Expand All @@ -833,13 +835,13 @@ async function createJob(
}).select("id").single();
if (error) {
console.error("Failed to create ingestion_jobs row:", error.message);
return 0;
return "";
}
return data?.id ?? 0;
return data?.id ?? "";
}

async function updateJobById(
jobId: number,
jobId: string,
updates: Record<string, unknown>,
): Promise<{ ok: boolean; error?: string }> {
const { data, error } = await supabase
Expand All @@ -849,21 +851,21 @@ async function updateJobById(
.select("id, status")
.maybeSingle();
if (error) {
console.error(`Failed to update job #${jobId}: ${error.message} (code: ${error.code}, details: ${error.details})`);
console.error(`Failed to update job ${jobId}: ${error.message} (code: ${error.code}, details: ${error.details})`);
return { ok: false, error: `${error.code}: ${error.message}` };
}
if (!data) {
console.error(`updateJobById: update matched 0 rows for job #${jobId}`);
return { ok: false, error: `No row matched for job #${jobId}` };
console.error(`updateJobById: update matched 0 rows for job ${jobId}`);
return { ok: false, error: `No row matched for job ${jobId}` };
}
return { ok: true };
}

async function persistItems(
jobId: number,
jobId: string,
items: IngestionItem[],
sourceMetadata?: Record<string, unknown> | null,
): Promise<number[]> {
): Promise<string[]> {
if (items.length === 0 || !jobId) return [];
const rows = items.map((item) => ({
job_id: jobId,
Expand All @@ -887,21 +889,26 @@ async function persistItems(
console.error("Failed to persist ingestion_items:", error.message);
return [];
}
return (data ?? []).map((row: { id: number }) => row.id);
return (data ?? []).map((row: { id: string }) => row.id);
}

// ── Execute a dry-run job ───────────────────────────────────────────────────

// OB1 ingestion_jobs.id is a UUID; reject anything that is not a UUID string.
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

async function handleExecuteJob(req: Request): Promise<Response> {
let body: Record<string, unknown>;
try { body = await req.json(); } catch { return json({ error: "Invalid JSON body" }, 400); }

const jobId = typeof body.job_id === "number" ? body.job_id : 0;
if (!jobId) return json({ error: "job_id is required" }, 400);
const jobId = typeof body.job_id === "string" && UUID_RE.test(body.job_id.trim())
? body.job_id.trim()
: "";
if (!jobId) return json({ error: "job_id is required (must be a UUID)" }, 400);

const { data: job, error: jobErr } = await supabase
.from("ingestion_jobs").select("*").eq("id", jobId).single();
if (jobErr || !job) return json({ error: `Job #${jobId} not found` }, 404);
if (jobErr || !job) return json({ error: `Job ${jobId} not found` }, 404);
if (job.status === "complete") return json({ ...job, message: "Job already complete" }, 200);
if (job.status !== "dry_run_complete") {
return json({ error: `Job status is '${job.status}', expected 'dry_run_complete'` }, 400);
Expand Down Expand Up @@ -1191,7 +1198,7 @@ Deno.serve(async (req) => {
}

// Persist items to ingestion_items table
let itemIds: number[] = [];
let itemIds: string[] = [];
if (jobId) itemIds = await persistItems(jobId, items, sourceMetadata);

if (dryRun) {
Expand Down Expand Up @@ -1234,7 +1241,7 @@ Deno.serve(async (req) => {
}
for (let i = 0; i < items.length; i++) {
const item = items[i];
const itemDbId = itemIds[i] ?? 0;
const itemDbId = itemIds[i] ?? "";
if (item.action === "skip") {
item.status = "executed";
if (itemDbId) await supabase.from("ingestion_items").update({ status: "executed" }).eq("id", itemDbId);
Expand Down
Loading