Skip to content

fix(watcher): stream oversized ingestion without loss - #631

Merged
EtanHey merged 7 commits into
mainfrom
fix/d1-oversized-ingestion
Aug 1, 2026
Merged

fix(watcher): stream oversized ingestion without loss#631
EtanHey merged 7 commits into
mainfrom
fix/d1-oversized-ingestion

Conversation

@EtanHey

@EtanHey EtanHey commented Aug 1, 2026

Copy link
Copy Markdown
Owner

What broke and how it was found

The watcher treated 100 MB as a correctness boundary: it advanced offsets to end-of-file without parsing oversized JSONL. Investigation found 17 abandoned files totaling 5.43 GB, including three created after the prior #613 fix.

Evidence

  • Removes the full-file offset advance; offsets now advance only over parsed bytes.
  • 17 affected files, 5,833,367,241 bytes; the two headline sessions contained 773 and 728 indexed chunks.
  • Focused watcher suite: 107 passed. Full Python run recorded 3,709 passed, 60 skipped, and 5 expected failures; two live-data failures were isolated as pre-existing/environmental.

Review

This branch was pair-reviewed by a separate agent under the taste gate: smallest correct solution, behavioural tests, and no speculative machinery. The D1 review also mutation-tested the defect: reintroducing the bad offset advance killed 10 tests.

Operational note

Enrichment and drain are currently disabled with launchctl to prevent provenance-tag decay. Re-enable deliberately after the provenance-column ownership question is settled.


Note

High Risk
Changes core ingestion checkpointing, offset registry semantics, and failure handling for multi-GB JSONL streams; mistakes could cause data loss, stuck files, or incorrect resume positions.

Overview
Fixes JSONL ingestion so large or problematic files are no longer advanced to EOF without parsing.

JSONLTailer reads in bounded windows (max_bytes / max_lines, 64 KiB chunks) with a separate BRAINLAYER_WATCH_MAX_RECORD_BYTES ceiling. Corrupt or oversized records stop parsing instead of being skipped; failed bytes stay in the buffer until quarantine or retry.

JSONLWatcher removes _skip_oversized_file. Offsets advance only over confirmed bytes, tagged with _source_inode and _source_generation so stale flush watermarks cannot move a replaced file. Malformed complete lines can be durably quarantined, alarmed, and advanced in order after prior bytes confirm. Health exposes ingestion failures and quarantine counts; watch-backfill keeps cycling while last_poll_made_progress is true even when a cycle returns zero indexed entries.

Reviewed by Cursor Bugbot for commit 16eb0ec. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Bug Fixes

    • Improved JSONL ingestion reliability for corrupt, oversized, and partially read records.
    • Prevented unconfirmed data from advancing processing checkpoints.
    • Added safer handling for file replacements, rewinds, read errors, and retries.
    • Ensured large files continue processing correctly across read windows.
  • Monitoring

    • Added ingestion failure and quarantine metrics, alarms, and expanded health details.
    • Invalid records can now be quarantined and tracked for recovery.

Note

Fix JSONL watcher to stream oversized records incrementally without data loss

  • JSONLTailer.read_new_lines now reads in 64 KiB chunks bounded by a per-poll byte window and line limit, buffering partial records instead of skipping or checkpointing past them.
  • JSONLTailer.read_buffered_lines stops at the first malformed or oversized record, exposes the failed bytes in failed_record, and does not advance the offset past them.
  • Malformed records are durably quarantined via JSONLWatcher._quarantine_failed_record, which persists the bytes to a content-addressed quarantine file, emits an alarm, and schedules offset advancement so polling continues.
  • OffsetRegistry.set now accepts a generation parameter to prevent offset regressions across file rewinds and inode changes.
  • JSONLWatcher.poll_once sets last_poll_made_progress so the watch-backfill CLI loop continues polling while large records are buffering, stopping only when both item count and progress are zero.
  • Behavioral Change: corrupt lines are no longer silently skipped; the tailer halts at the first bad record and requires explicit quarantine before advancing.

Macroscope summarized 16eb0ec.

EtanHey and others added 4 commits August 1, 2026 01:41
A worker-authored 'Verdict: ACCEPT' is not acceptance (plan Phase 7;
collab constraint 4). These 1,047 lines were the implementer grading
its own work and must not reach a PR. Review is performed by a fresh
Claude seat, not the implementer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your trial has ended. Reactivate Greptile to resume code reviews.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

JSONL ingestion

Layer / File(s) Summary
Bounded tailing and retained parse failures
src/brainlayer/watcher.py, tests/test_jsonl_watcher.py
JSONL reads now use bounded byte windows and record limits. Invalid and oversized records remain available for handling. Tests cover buffering, large files, limits, and appends.
Generation-aware batch confirmation
src/brainlayer/watcher.py, tests/test_jsonl_watcher.py
Batch confirmations now include source entries. Offset advancement checks inode and rewind generation. Tests cover replacement files, rewinds, retries, and delayed confirmations.
Quarantine and ingestion health reporting
src/brainlayer/watcher.py, tests/test_jsonl_watcher.py
Malformed records can be atomically quarantined. Ingestion failures, quarantine totals, alarms, rollback, and bounded health details are recorded and tested.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant JSONLWatcher
  participant JSONLTailer
  participant BatchIndexer
  participant OffsetRegistry
  JSONLWatcher->>JSONLTailer: Read a bounded byte window
  JSONLTailer-->>JSONLWatcher: Return parsed entries or retained failure
  JSONLWatcher->>BatchIndexer: Process the source batch
  BatchIndexer-->>JSONLWatcher: Return confirmed watermarks
  JSONLWatcher->>OffsetRegistry: Advance matching inode and generation
Loading

Possibly related PRs

Poem

I’m a rabbit watching each line,
Through bounded reads, neat and fine.
Bad records hop to quarantine,
Confirmed offsets keep their line.
Health alarms now softly glow—
Safe tails tell us where to go.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: streaming oversized JSONL ingestion without data loss.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/d1-oversized-ingestion

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread src/brainlayer/watcher.py Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 31127db7f4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/brainlayer/watcher.py
Comment on lines +1598 to +1601
new_lines = tailer.read_new_lines(
max_lines=self.max_lines_per_file,
max_bytes=self.max_read_bytes_per_file,
)

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 Keep backfill polling while a record is partial

When a valid JSONL record exceeds BRAINLAYER_WATCH_MAX_FILE_BYTES but remains below the record ceiling (100–128 MB with the defaults), this bounded read buffers only the first window and poll_once() returns zero normalized lines. The watch-backfill loop in src/brainlayer/cli/__init__.py:3494-3498 interprets that zero as completion and exits with the offset still at zero, so the oversized record is never indexed even though another poll would continue it. Distinguish partial read progress from an idle watcher before terminating the backfill loop.

AGENTS.md reference: AGENTS.md:L5-L7

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/brainlayer/watcher.py`:
- Around line 1378-1386: Update the watchdog alerting logic near the
`alert_reasons` construction to derive quarantine activity from recent entries
in `self._quarantined_records`, using the newest `observed_at` within the
current health window rather than lifetime
`self._quarantined_record_count_total`. Use that recent-activity flag for both
the `quarantined_record` reason and the `alerting` boolean, while retaining
`quarantined_record_count_total` solely as an informational counter.
- Around line 1228-1256: Update the quarantine handling around
_quarantine_failed_record to enforce retention for existing .bad files using an
age- or size-based prune, while preserving the current write and collision
behavior. When creating BRAINLAYER_WATCHER_QUARANTINE_DIR, apply a restrictive
owner-only directory mode and ensure pruning does not remove newly written or
unrelated files.
- Around line 1636-1642: Update the last_error attribute annotation in the
tailer class to BaseException | None so it accurately accepts the arbitrary
Exception assigned in the polling error handler. Leave the existing assignment
and runtime type checks unchanged.
- Around line 700-731: Update the buffer handling in the watcher read flow
around _partial_record_bytes and the read loop to track whether f.read()
appended any bytes. Only create or write back the combined buffer when data was
read; when the first or subsequent read returns empty, retain self._buffer
unchanged and avoid both full-buffer copies.
- Around line 1006-1018: Ensure watcher offset and quarantine state has
single-thread ownership or is protected by one shared lock: update the
poll_once/CLI flush path and BatchIndexer._do_flush callback path, including
_advance_confirmed_batch, OffsetRegistry updates, and watcher dictionaries such
as _file_ingestion_failures and _pending_quarantined_offsets. Apply the same
synchronization to every read-modify-write path so concurrent callers cannot
mutate these structures unsafely.
- Around line 1216-1222: Update _quarantine_failed_record to also accept
OversizedJSONLRecordError in its tailer.last_error type check, so complete
oversized records with a failed_record are quarantined and checkpointed through
the existing poll_once flow.

In `@tests/test_jsonl_watcher.py`:
- Around line 1738-1768: Add coverage alongside
test_quarantined_offset_waits_for_prior_indexable_record_confirmation for two
pending malformed ranges separated by a valid record, asserting sorting,
retention, and consecutive advancement behavior in _advance_quarantined_offsets;
add a separate rewind scenario with a pending quarantine range, asserting
_handle_rewind clears it and does not advance the offset over truncated or
replaced bytes.
- Line 624: Update the test assertion around tailer.last_error to import
OversizedJSONLRecordError from brainlayer.watcher and use
isinstance(tailer.last_error, OversizedJSONLRecordError) instead of comparing
the runtime type name string.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a56c1c0f-1904-4b7c-befa-b3b113ce37ec

📥 Commits

Reviewing files that changed from the base of the PR and between 0a8cbbf and 31127db.

📒 Files selected for processing (2)
  • src/brainlayer/watcher.py
  • tests/test_jsonl_watcher.py
📜 Review details
⏰ Context from checks skipped due to timeout. (5)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Macroscope - Correctness Check
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.12)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Flag risky DB or concurrency changes explicitly and do not hand-wave lock behavior
Enforce one-write-at-a-time concurrency constraint; reads are safe but brain_digest is write-heavy and must not run in parallel with other MCP work
Run pytest before claiming behavior changed safely; current test suite has 929 tests

Files:

  • tests/test_jsonl_watcher.py
  • src/brainlayer/watcher.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Tests must not refresh the production backup heartbeat log; use BRAINLAYER_BACKUP_LOG_PATH and set provenance to pytest.

Files:

  • tests/test_jsonl_watcher.py
src/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.py: Use paths.py:get_db_path() for all database path resolution; do not hard-code the BrainLayer database path.
Each worker must use its own database connection and retry on SQLITE_BUSY.
For bulk database operations, stop enrichment workers, checkpoint WAL before and after, drop and recreate FTS triggers around large deletes, batch deletes in 5–10K chunks, and checkpoint every three batches.
Use the canonical BrainLayer naming: BrainLayer (זיכרון) means “memory.”

Files:

  • src/brainlayer/watcher.py
src/brainlayer/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/brainlayer/**/*.py: Preserve ai_code, stack_trace, and user_message verbatim; skip noise, summarize build logs, and retain only structure for directory listings.
Use AST-aware tree-sitter chunking, never split stack traces, and mask large tool output.
Keep Groq as the primary enrichment backend, Gemini as fallback, and Ollama as the offline last resort; honor BRAINLAYER_ENRICH_BACKEND and BRAINLAYER_ENRICH_RATE.
Default searches must exclude lifecycle-managed chunks; include_archived=True may expose history. brain_supersede must apply its personal-data safety gate, and brain_archive must soft-delete with a timestamp.

Files:

  • src/brainlayer/watcher.py
src/brainlayer/watcher*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/brainlayer/watcher*.py: The JSONL watcher must apply filters in order: entry-type whitelist, classification, minimum chunk length, and system-reminder stripping.
Persist watcher offsets, detect file rewinds, and soft-archive chunks reverted by checkpoint restoration.

Files:

  • src/brainlayer/watcher.py
🪛 ast-grep (0.45.0)
tests/test_jsonl_watcher.py

[info] 596-596: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"role": "user", "content": "x" * 256})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 605-605: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"role": "user", "content": "x" * 2048})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 729-729: Do not hardcode temporary file or directory names
Context: "/tmp/source.jsonl"
Note: [CWE-377] Insecure Temporary File.

(hardcoded-tmp-file)


[info] 1285-1285: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"role": "user", "content": content})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 1449-1449: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"role": "user", "content": "replacement"})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 1463-1463: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"role": "user", "content": content})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 1491-1491: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"role": "user", "content": f"new-{index}"})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 1504-1504: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"role": "user", "content": "new-append"})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 1615-1615: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"role": "user", "content": "must not be checkpointed"})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 1651-1651: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"role": "user", "content": content})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 1694-1694: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"role": "user", "content": "first"})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 1696-1696: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"role": "user", "content": "last"})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 1741-1741: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"role": "user", "content": "first"})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 1837-1837: Do not hardcode temporary file or directory names
Context: f"/tmp/failure-{index}.jsonl"
Note: [CWE-377] Insecure Temporary File.

(hardcoded-tmp-file)


[info] 1838-1838: Do not hardcode temporary file or directory names
Context: f"/tmp/failure-{index}.jsonl"
Note: [CWE-377] Insecure Temporary File.

(hardcoded-tmp-file)


[info] 1900-1900: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"role": "user", "content": "small append"})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

src/brainlayer/watcher.py

[warning] 696-696: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(self.filepath, "rb")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🔇 Additional comments (19)
src/brainlayer/watcher.py (11)

51-59: LGTM!

Also applies to: 71-71, 82-99


364-367: LGTM!


634-657: LGTM!


744-807: LGTM!


882-883: LGTM!

Also applies to: 929-945


1038-1081: LGTM!


1167-1214: LGTM!

Also applies to: 1257-1293


1369-1377: LGTM!

Also applies to: 1405-1409


1460-1480: LGTM!

Also applies to: 1494-1494


1532-1542: LGTM!

Also applies to: 1556-1578, 1592-1601, 1616-1621, 1630-1635


826-836: 🗄️ Data Integrity & Integration

No remaining on_confirm_offsets callers remain.

tests/test_jsonl_watcher.py (8)

582-623: LGTM!

Also applies to: 625-625


727-742: LGTM!


1281-1308: LGTM!


1333-1336: LGTM!

Also applies to: 1363-1386


1415-1424: LGTM!

Also applies to: 1426-1457, 1459-1508


1521-1528: LGTM!

Also applies to: 1539-1544


1612-1736: LGTM!

Also applies to: 1770-1851


1853-1919: LGTM!

Comment thread src/brainlayer/watcher.py
Comment on lines +700 to +731
complete_lines = self._buffer.count(b"\n")
current_record_bytes = self._partial_record_bytes()
combined = bytearray(self._buffer)
while remaining_bytes is None or remaining_bytes > 0:
if max_lines is not None and complete_lines >= max_lines:
break
chunk_bytes = _WATCH_READ_CHUNK_BYTES
if remaining_bytes is not None:
chunk_bytes = min(chunk_bytes, remaining_bytes)
if self.max_record_bytes is not None:
record_capacity = self.max_record_bytes - current_record_bytes + 1
if record_capacity <= 0:
break
chunk_bytes = min(chunk_bytes, record_capacity)
new_data = f.read(chunk_bytes)
if not new_data:
break
combined.extend(new_data)
if remaining_bytes is not None:
remaining_bytes -= len(new_data)

cursor = 0
while True:
newline_index = new_data.find(b"\n", cursor)
if newline_index < 0:
current_record_bytes += len(new_data) - cursor
break
current_record_bytes += newline_index - cursor
complete_lines += 1
current_record_bytes = 0
cursor = newline_index + 1
self._buffer = bytes(combined)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid copying the whole buffer when no new bytes arrive.

Line 702 copies self._buffer into combined, and line 731 copies it back with bytes(combined). Both copies run even when f.read() returns no data on the first iteration.

A retained partial record can reach max_record_bytes (128 MiB by default). A blocked or slow-growing record therefore triggers two full-buffer copies on every poll for that file, at the poll interval. Track whether any bytes were appended and skip the write-back when nothing changed.

♻️ Proposed fix to skip the copy on an empty read
                 complete_lines = self._buffer.count(b"\n")
                 current_record_bytes = self._partial_record_bytes()
-                combined = bytearray(self._buffer)
+                combined: bytearray | None = None
                 while remaining_bytes is None or remaining_bytes > 0:
@@
                     new_data = f.read(chunk_bytes)
                     if not new_data:
                         break
+                    if combined is None:
+                        combined = bytearray(self._buffer)
                     combined.extend(new_data)
@@
-                self._buffer = bytes(combined)
+                if combined is not None:
+                    self._buffer = bytes(combined)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
complete_lines = self._buffer.count(b"\n")
current_record_bytes = self._partial_record_bytes()
combined = bytearray(self._buffer)
while remaining_bytes is None or remaining_bytes > 0:
if max_lines is not None and complete_lines >= max_lines:
break
chunk_bytes = _WATCH_READ_CHUNK_BYTES
if remaining_bytes is not None:
chunk_bytes = min(chunk_bytes, remaining_bytes)
if self.max_record_bytes is not None:
record_capacity = self.max_record_bytes - current_record_bytes + 1
if record_capacity <= 0:
break
chunk_bytes = min(chunk_bytes, record_capacity)
new_data = f.read(chunk_bytes)
if not new_data:
break
combined.extend(new_data)
if remaining_bytes is not None:
remaining_bytes -= len(new_data)
cursor = 0
while True:
newline_index = new_data.find(b"\n", cursor)
if newline_index < 0:
current_record_bytes += len(new_data) - cursor
break
current_record_bytes += newline_index - cursor
complete_lines += 1
current_record_bytes = 0
cursor = newline_index + 1
self._buffer = bytes(combined)
complete_lines = self._buffer.count(b"\n")
current_record_bytes = self._partial_record_bytes()
combined: bytearray | None = None
while remaining_bytes is None or remaining_bytes > 0:
if max_lines is not None and complete_lines >= max_lines:
break
chunk_bytes = _WATCH_READ_CHUNK_BYTES
if remaining_bytes is not None:
chunk_bytes = min(chunk_bytes, remaining_bytes)
if self.max_record_bytes is not None:
record_capacity = self.max_record_bytes - current_record_bytes + 1
if record_capacity <= 0:
break
chunk_bytes = min(chunk_bytes, record_capacity)
new_data = f.read(chunk_bytes)
if not new_data:
break
if combined is None:
combined = bytearray(self._buffer)
combined.extend(new_data)
if remaining_bytes is not None:
remaining_bytes -= len(new_data)
cursor = 0
while True:
newline_index = new_data.find(b"\n", cursor)
if newline_index < 0:
current_record_bytes += len(new_data) - cursor
break
current_record_bytes += newline_index - cursor
complete_lines += 1
current_record_bytes = 0
cursor = newline_index + 1
if combined is not None:
self._buffer = bytes(combined)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/brainlayer/watcher.py` around lines 700 - 731, Update the buffer handling
in the watcher read flow around _partial_record_bytes and the read loop to track
whether f.read() appended any bytes. Only create or write back the combined
buffer when data was read; when the first or subsequent read returns empty,
retain self._buffer unchanged and avoid both full-buffer copies.

Comment thread src/brainlayer/watcher.py Outdated
Comment thread src/brainlayer/watcher.py Outdated
Comment thread src/brainlayer/watcher.py
Comment on lines +1228 to +1256
quarantine_dir = Path(
os.environ.get("BRAINLAYER_WATCHER_QUARANTINE_DIR", "~/.brainlayer/quarantine")
).expanduser()
quarantine_path = quarantine_dir / (
f"watcher-parse-{Path(filepath).stem}-{start_offset}-{digest[:16]}.jsonl.bad"
)
temp_path: Path | None = None
try:
quarantine_dir.mkdir(parents=True, exist_ok=True)
if not quarantine_path.exists():
with tempfile.NamedTemporaryFile(
mode="wb",
dir=quarantine_dir,
prefix=f".{quarantine_path.name}.",
delete=False,
) as handle:
temp_path = Path(handle.name)
handle.write(record)
handle.flush()
os.fsync(handle.fileno())
temp_path.replace(quarantine_path)
temp_path = None
directory_fd = os.open(quarantine_dir, os.O_RDONLY)
try:
os.fsync(directory_fd)
finally:
os.close(directory_fd)
elif quarantine_path.read_bytes() != record:
raise OSError(f"quarantine collision at {quarantine_path}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Add a retention policy for the quarantine directory.

_quarantine_failed_record writes raw transcript bytes to BRAINLAYER_WATCHER_QUARANTINE_DIR and never removes them. _quarantined_records is capped in memory at _MAX_HEALTH_QUARANTINE_DETAILS, but the .bad files on disk grow without a bound and without an expiry.

The quarantined bytes are unredacted user transcript content, and the file name embeds the session id through Path(filepath).stem. This content sits outside the lifecycle-managed store, so archive and supersede paths never reach it.

Add an age-based or size-based prune for the quarantine directory, and restrict the directory mode when it is created.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/brainlayer/watcher.py` around lines 1228 - 1256, Update the quarantine
handling around _quarantine_failed_record to enforce retention for existing .bad
files using an age- or size-based prune, while preserving the current write and
collision behavior. When creating BRAINLAYER_WATCHER_QUARANTINE_DIR, apply a
restrictive owner-only directory mode and ensure pruning does not remove newly
written or unrelated files.

Source: Coding guidelines

Comment thread src/brainlayer/watcher.py
Comment on lines +1378 to +1386
if self._quarantined_record_count_total and "quarantined_record" not in alert_reasons:
alert_reasons.append("quarantined_record")
watchdog = {
**watchdog,
"alerting": bool(watchdog.get("alerting"))
or bool(all_failure_payloads)
or bool(self._quarantined_record_count_total),
"alert_reasons": alert_reasons,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

alerting stays true forever after one quarantined record.

self._quarantined_record_count_total is a lifetime counter and never resets. Lines 1378-1384 therefore force alerting: True and add quarantined_record to alert_reasons for the whole process lifetime, even after ingestion recovers.

_file_ingestion_failures is pruned each poll and clears correctly, so quarantine is the only permanently sticky reason. Downstream alerting cannot distinguish an active fault from one historical quarantine.

Gate the alert on recent quarantine activity, for example on the newest observed_at in self._quarantined_records within the current health window, and keep quarantined_record_count_total as an informational counter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/brainlayer/watcher.py` around lines 1378 - 1386, Update the watchdog
alerting logic near the `alert_reasons` construction to derive quarantine
activity from recent entries in `self._quarantined_records`, using the newest
`observed_at` within the current health window rather than lifetime
`self._quarantined_record_count_total`. Use that recent-activity flag for both
the `quarantined_record` reason and the `alerting` boolean, while retaining
`quarantined_record_count_total` solely as an informational counter.

Comment thread src/brainlayer/watcher.py
Comment on lines +1636 to +1642
except Exception as error:
if tailer is not None and tailer_snapshot is not None and not read_accepted:
tailer.offset, tailer._buffer = tailer_snapshot
tailer.last_error = error
tailer.failed_record = None
logger.exception("Poll file error: %s", filepath)
self._record_file_ingestion_failure(filepath, error)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Widen the last_error annotation to match this assignment.

Line 1639 assigns an arbitrary caught Exception to tailer.last_error. Line 655 declares the attribute as OSError | json.JSONDecodeError | UnicodeDecodeError | OversizedJSONLRecordError | None. A RuntimeError from _normalize_lines therefore violates the declared type.

Runtime behavior is safe, because _quarantine_failed_record re-checks the type with isinstance. Update the annotation on line 655 to BaseException | None, so type checkers stay accurate.

♻️ Proposed annotation fix at line 655
-        self.last_error: OSError | json.JSONDecodeError | UnicodeDecodeError | OversizedJSONLRecordError | None = None
+        self.last_error: BaseException | None = None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/brainlayer/watcher.py` around lines 1636 - 1642, Update the last_error
attribute annotation in the tailer class to BaseException | None so it
accurately accepts the arbitrary Exception assigned in the polling error
handler. Leave the existing assignment and runtime type checks unchanged.

assert tailer.read_new_lines(max_bytes=1024) == []

assert len(tailer._buffer) <= 4097
assert type(tailer.last_error).__name__ == "OversizedJSONLRecordError"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the exception type directly.

The test compares type(tailer.last_error).__name__ to a string. A rename of OversizedJSONLRecordError then leaves the test passing against the wrong type, or failing with an unclear message. Import the class and use isinstance.

♻️ Proposed assertion change
-        assert type(tailer.last_error).__name__ == "OversizedJSONLRecordError"
+        assert isinstance(tailer.last_error, OversizedJSONLRecordError)

Add the symbol to the existing import block near line 24:

from brainlayer.watcher import OversizedJSONLRecordError
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_jsonl_watcher.py` at line 624, Update the test assertion around
tailer.last_error to import OversizedJSONLRecordError from brainlayer.watcher
and use isinstance(tailer.last_error, OversizedJSONLRecordError) instead of
comparing the runtime type name string.

Comment on lines +1738 to +1768
def test_quarantined_offset_waits_for_prior_indexable_record_confirmation(self, tmp_path, monkeypatch):
sessions = tmp_path / "codex" / "sessions"
sessions.mkdir(parents=True)
rollout = sessions / "rollout.jsonl"
first = json.dumps({"role": "user", "content": "first"}) + "\n"
malformed = b"not json at all\n"
rollout.write_bytes(first.encode() + malformed)
monkeypatch.setenv("BRAINLAYER_WATCHER_QUARANTINE_DIR", str(tmp_path / "quarantine"))

def confirm_all(items):
return {item["_source_file"]: item["_line_end_offset"] for item in items}

def capture_alarm(code, message, context):
raise BrainLayerAlarm(code, message, context)

monkeypatch.setattr("brainlayer.watcher.raise_alarm", capture_alarm)

watcher = JSONLWatcher(
watch_roots=[WatchRoot("codex", sessions)],
registry_path=tmp_path / "offsets.json",
on_flush=confirm_all,
batch_size=10,
flush_interval_ms=360_000,
)

watcher.poll_once()
assert watcher.registry.get(str(rollout)) == (0, 0)

watcher.indexer.flush()

assert watcher.registry.get(str(rollout))[0] == rollout.stat().st_size

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add coverage for multiple pending quarantine ranges and for rewind clearing.

_advance_quarantined_offsets sorts a list of pending ranges and advances over consecutive ranges. This test exercises exactly one pending range, so the sort, the remaining retention branch, and the multi-range advance stay uncovered.

_pending_quarantined_offsets.pop(filepath, None) also runs in _ensure_tailer on inode change and in _handle_rewind. No test asserts that a pending range is dropped after a rewind, which would otherwise advance an offset over bytes that no longer exist.

Add two tests: one file with two malformed records separated by a valid record, and one file where a rewind occurs while a quarantined range is still pending.

🧰 Tools
🪛 ast-grep (0.45.0)

[info] 1741-1741: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"role": "user", "content": "first"})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_jsonl_watcher.py` around lines 1738 - 1768, Add coverage alongside
test_quarantined_offset_waits_for_prior_indexable_record_confirmation for two
pending malformed ranges separated by a valid record, asserting sorting,
retention, and consecutive advancement behavior in _advance_quarantined_offsets;
add a separate rewind scenario with a pending quarantine range, asserting
_handle_rewind clears it and does not advance the offset over truncated or
replaced bytes.

@EtanHey

EtanHey commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

VERDICT: ITERATE
TASTE: The core fix remains causally justified and the follow-up commit 31127db removes all three accepted first-review artifacts: REPORT.md, the dead max_file_bytes alias, and the dead on_confirm_offsets fallback. No new scenic machinery was added. Two cross-path correctness gaps still make the solution incomplete.
TESTS: Exact head 31127db. Focused watcher suite: 107 passed. Full suite: 3709 passed, 60 skipped, 5 xfailed, 2 failed in the live TestSessionsReal checks. Bug-reintroduction mutation restored the old skip-to-EOF behavior: both load-bearing oversized-ingestion tests failed; after restoring the head, both passed. Two new isolated behavioral repros against the unmodified head both failed: watch-backfill stopped after one partial read, and old-inode confirmation persisted the stale offset with the replacement inode. Ruff check, Ruff format check, and git diff --check passed. Local CodeRabbit gate: SKIPPED — bounded 180-second timeout; red-team and blue-team fallback review performed.
MISSED BY FIRST REVIEW: (1) watch-backfill treats poll_once() == 0 as EOF at src/brainlayer/cli/init.py:3494-3498 even though the new bounded read at src/brainlayer/watcher.py:1598-1601 can return zero after making buffer progress. With a 128-byte window and one 292-byte valid record, the command reported processed_entries=0, cycles=1, left the registry at zero, and exited. (2) Generation filtering is followed by a fresh inode lookup: src/brainlayer/watcher.py:1040-1060 validates old batch provenance, then src/brainlayer/watcher.py:1031-1038 can write that old offset with a replacement inode. The quarantine path repeats the same pairing at src/brainlayer/watcher.py:1062-1077. Replacing the file after poll and before flush persisted (old_size, new_inode); a restarted watcher then trusted the stale offset. All citations are at 31127db.
BLOCKERS: 1. Make watch-backfill distinguish buffered/read progress from a truly idle watcher, and add an integration regression with one valid record larger than the read window. 2. Carry the already-validated source inode/generation through the registry write instead of re-reading the current inode; make pending quarantine ranges generation-aware too. Add a replacement-between-read-and-confirmation test that flushes the registry, constructs a fresh watcher, and proves the replacement starts at offset zero.

@cursor

cursor Bot commented Aug 1, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_cb43ad0d-16c6-4678-a3c2-03c1f2478238)

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your trial has ended. Reactivate Greptile to resume code reviews.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7e61455a06

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/brainlayer/watcher.py
self.rewind_new_offset = 0
self.last_error: OSError | json.JSONDecodeError | UnicodeDecodeError | OversizedJSONLRecordError | None = None
self.failed_record: bytes | None = None
self.observed_inode = self.get_inode()

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 Capture the inode from the opened file

When a watched path is atomically replaced after this constructor's stat but before read_new_lines() opens it, the bytes come from the replacement while _source_inode remains the old inode. The first flush is therefore accepted under the stale generation, and the next poll detects the inode mismatch, rewinds to zero, and queues the same replacement records again. Derive the source inode with fstat from the descriptor used for reading so the bytes and generation describe the same file.

AGENTS.md reference: AGENTS.md:L5-L7

Useful? React with 👍 / 👎.

@cursor

cursor Bot commented Aug 1, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_4429eaec-9056-42cf-ae2d-dc0cf7f9e658)

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your trial has ended. Reactivate Greptile to resume code reviews.

@cursor

cursor Bot commented Aug 1, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_6df053ff-2476-4fb4-bb59-ccb0084d4fd7)

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your trial has ended. Reactivate Greptile to resume code reviews.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 16eb0ec295

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/brainlayer/watcher.py
Comment on lines +1085 to +1087
if current_offset < start_offset:
remaining.append((start_offset, end_offset, pending_inode, pending_generation))
continue

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 Preserve skipped ranges until quarantines can checkpoint

When one poll reads an indexable record, a provider record that normalization intentionally drops, and then a malformed record, delayed batch confirmation checkpoints only the indexable record. The quarantine starts after the dropped record, so this branch retains it because current_offset < start_offset, but the dropped range is no longer tracked and later idle polls cannot close the gap. The live watcher therefore leaves the registry permanently behind the tailer; after restart it reprocesses the dropped record and quarantines/alarms on the same malformed record again. Persist the discarded range or retry its checkpoint when the preceding batch confirms.

AGENTS.md reference: AGENTS.md:L5-L7

Useful? React with 👍 / 👎.

@EtanHey
EtanHey merged commit 133c025 into main Aug 1, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant