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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)

@cubic-dev-ai cubic-dev-ai Bot Jun 12, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: README now renders duplicate badges because a new badge block was added without removing the existing one.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At README.md, line 3:

<comment>README now renders duplicate badges because a new badge block was added without removing the existing one.</comment>

<file context>
@@ -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)
</file context>
Fix with cubic

[![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)
Expand Down Expand Up @@ -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).
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
44 changes: 32 additions & 12 deletions src/ingest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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<void> {
const filePath = walked.path;
const language = walked.language;
const fileStart = Date.now();
Expand All @@ -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)
Expand All @@ -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 ────────────────────────
Expand All @@ -211,7 +214,7 @@ export async function ingestPath(
result.filesSkipped++;
result.filesProcessed++;
options?.progressCallback?.({ current: result.filesProcessed, total: walkedFiles.length });
continue;
return;
}
}

Expand All @@ -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 ──────────────────────────────────
Expand Down Expand Up @@ -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) ──
Expand All @@ -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) ───────────────────────
Expand Down Expand Up @@ -328,7 +331,7 @@ export async function ingestPath(

if (fileFailed) {
result.filesProcessed++;
continue;
return;
}

// ── 10. Embed chunks (vec0) — non-fatal if unavailable ─────────
Expand All @@ -345,7 +348,7 @@ export async function ingestPath(

if (fileFailed) {
result.filesProcessed++;
continue;
return;
}

// ── 11. Populate symbols table (global symbol store) ─────────────
Expand Down Expand Up @@ -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([

@cubic-dev-ai cubic-dev-ai Bot Jun 12, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Promise.race timeout does not cancel processFile, so timed-out files can keep writing to DB and mutating counters after being marked failed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/ingest/index.ts, line 393:

<comment>`Promise.race` timeout does not cancel `processFile`, so timed-out files can keep writing to DB and mutating counters after being marked failed.</comment>

<file context>
@@ -383,10 +386,27 @@ export async function ingestPath(
+
+    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))
</file context>
Fix with cubic

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);
Expand Down
Loading