From 9e7e40d1a83fce9cc88dc08d351085d986b0c603 Mon Sep 17 00:00:00 2001 From: 4 Bytes Robby Date: Thu, 11 Jun 2026 20:32:44 +0200 Subject: [PATCH 1/2] fix: add 10s per-file timeout to ingest to prevent hangs on large repos --- package.json | 2 +- src/ingest/index.ts | 44 ++++++++++++++++++++++++++++++++------------ 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 6a3e304..d92bfd3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@four-bytes/four-opencode-brain", - "version": "1.7.3", + "version": "1.7.4", "description": "Unified brain plugin — single SQLite DB for RAG search, memory, and knowledge base", "license": "Apache-2.0", "type": "module", diff --git a/src/ingest/index.ts b/src/ingest/index.ts index 66a7e8b..c7a3e24 100644 --- a/src/ingest/index.ts +++ b/src/ingest/index.ts @@ -61,6 +61,9 @@ export interface IngestOptions { /** Maximum file size for ingestion (10 MB). Files larger than this are skipped. */ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB +/** Per-file processing timeout (10 seconds). */ +const FILE_TIMEOUT_MS = 10_000; // 10 seconds per file + // --------------------------------------------------------------------------- // Progress event helpers (gated on BRAIN_DEBUG=true) // --------------------------------------------------------------------------- @@ -147,7 +150,7 @@ export async function ingestPath( // Yield so event loop can handle HTTP requests + tool calls before ingest starts await new Promise(r => setTimeout(r, 0)); - for (const [i, walked] of walkedFiles.entries()) { + async function processFile(walked: WalkResult, i: number): Promise { const filePath = walked.path; const language = walked.language; const fileStart = Date.now(); @@ -164,14 +167,14 @@ export async function ingestPath( fileStats = await stat(filePath); } catch (err) { result.errors.push(`Failed to stat ${filePath}: ${String(err)}`); - continue; + return; } if (fileStats.size > MAX_FILE_SIZE) { result.errors.push( `Skipped ${filePath}: file size ${fileStats.size} exceeds 10MB cap`, ); - continue; + return; } // Read file as raw buffer — binary-safe hashing (avoids encoding issues) @@ -181,13 +184,13 @@ export async function ingestPath( } catch (err) { result.errors.push(`Failed to read ${filePath}: ${String(err)}`); result.filesProcessed++; - continue; + return; } // Binary content guard: skip files with null bytes (safety net for misnamed binaries) if (isBinaryContent(new Uint8Array(buf))) { log("debug", "ingest", `Skipped binary content: ${filePath}`); - continue; + return; } // ── Per-file SAVEPOINT for atomic DB writes ──────────────────────── @@ -211,7 +214,7 @@ export async function ingestPath( result.filesSkipped++; result.filesProcessed++; options?.progressCallback?.({ current: result.filesProcessed, total: walkedFiles.length }); - continue; + return; } } @@ -237,7 +240,7 @@ export async function ingestPath( } catch (err) { db.exec(`ROLLBACK TO SAVEPOINT ${spFile}`); result.errors.push(`Failed to insert file ${filePath}: ${String(err)}`); - continue; + return; } // ── 7. Insert documents table ────────────────────────────────── @@ -265,7 +268,7 @@ export async function ingestPath( } catch (err) { db.exec(`ROLLBACK TO SAVEPOINT ${spFile}`); result.errors.push(`Failed to insert document ${filePath}: ${String(err)}`); - continue; + return; } // ── 8. Chunk content (symbol extraction happens inside chunker) ── @@ -284,7 +287,7 @@ export async function ingestPath( } catch (err) { db.exec(`ROLLBACK TO SAVEPOINT ${spFile}`); result.errors.push(`Failed to chunk ${filePath}: ${String(err)}`); - continue; + return; } // ── 9. Insert chunks (includes token_count) ─────────────────────── @@ -328,7 +331,7 @@ export async function ingestPath( if (fileFailed) { result.filesProcessed++; - continue; + return; } // ── 10. Embed chunks (vec0) — non-fatal if unavailable ───────── @@ -345,7 +348,7 @@ export async function ingestPath( if (fileFailed) { result.filesProcessed++; - continue; + return; } // ── 11. Populate symbols table (global symbol store) ───────────── @@ -383,10 +386,27 @@ export async function ingestPath( if (process.env.BRAIN_DEBUG === "true") { log("debug", "ingest", `processed ${result.filesProcessed}/${walkedFiles.length}: ${filePath} (${Date.now() - fileStart}ms)`); } + } + + for (const [i, walked] of walkedFiles.entries()) { + try { + await Promise.race([ + processFile(walked, i), + new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), FILE_TIMEOUT_MS)) + ]); + } catch (err) { + if (err instanceof Error && err.message === 'timeout') { + emitProgressEvent("ingest.file_timeout", { file: walked.path }); + result.errors.push(`Timeout processing ${walked.path}: exceeded ${FILE_TIMEOUT_MS}ms`); + result.filesProcessed++; + continue; + } + throw err; + } // Yield so event loop can handle HTTP requests + tool calls await new Promise(r => setTimeout(r, 0)); - } + } // Checkpoint WAL to keep file manageable after large ingests checkpointDatabase(db); From aeb4e8547420609bffe48013dcd86ff8f7c9f402 Mon Sep 17 00:00:00 2001 From: 4 Bytes Robby Date: Fri, 12 Jun 2026 07:39:35 +0200 Subject: [PATCH 2/2] docs: add badges + star CTA to README --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 119112b..5ec2962 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # 🧠 four-opencode-brain +[![npm](https://img.shields.io/npm/v/@four-bytes/four-opencode-brain)](https://www.npmjs.com/package/@four-bytes/four-opencode-brain) +[![license](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE) +[![bun](https://img.shields.io/badge/runtime-bun-orange)](https://bun.sh) + **Unified brain plugin for [opencode](https://github.com/sst/opencode)** — single SQLite database for RAG search, memory, and knowledge base. [![npm](https://img.shields.io/npm/v/@four-bytes/four-opencode-brain)](https://www.npmjs.com/package/@four-bytes/four-opencode-brain) @@ -101,3 +105,7 @@ src/ ## License Apache-2.0 © [Four Bytes / Four Flames GmbH & Co. KG](https://fourbytes.de) + +--- + +> If this plugin saves you tokens, consider leaving a ⭐ on [GitHub](https://github.com/four-bytes/four-opencode-brain).