Skip to content

Fix recall_file panic on out-of-range lines; guard each request - #57

Merged
HackPoint merged 5 commits into
mainfrom
agents/recall-file-range-panic
Oct 13, 2026
Merged

HackPoint merged 5 commits into
mainfrom
agents/recall-file-range-panic

Conversation

@HackPoint

@HackPoint HackPoint commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

What and why

recall_file with a start_line five or more lines past the end of the file, or well after its end_line, panicked on src_lines[ctx_start..ctx_end]. The server exited with status 101 and never answered again, so one bad guess took every Lumen tool away for the rest of the session.

  • One function (range_window) now resolves the window. A range the file cannot satisfy is an error reply that states the file's line count and what was asked. An end_line past the last line clamps to it. The arithmetic saturates (u64::MAX, i64::MAX).
  • Each request now runs under a panic guard, so a bug elsewhere cannot end the server the same way.

The range error is a JSON-RPC error frame (-32602, like the other argument errors in lib.rs), not an isError: true result: lib.rs has no such reply for a tool failure. A caught panic is -32603.

Red to green

Real binary over stdio (crates/lumen-mcp/tests/recall_range.rs). After every bad request a valid one is sent and must be answered. Written first, run against the unfixed code: 5 failed, 3 passed.

request (200-line file) before after
start 100000, end 100010 server exits 101 -32602 naming the line count and the range; still serving
start 150, end 10 exits 101 -32602
start u64::MAX / i64::MAX exits 101 -32602
start 201 to 205 201-204 answered with the last lines or nothing, 205 exits 101 -32602
a range from line 1 on an empty file whole-file reply costing more than the file -32602 ("0 lines")
end past EOF, start 0, non-integers pass pass, unchanged

Unit tests on the window logic cannot compile against the old inline code, so they ran against a stub restating the old arithmetic: 9 failed, 113 passed. The lib now has 135 passing.

Valid requests are byte-identical

The base-commit binary against this branch, 48 requests over rs, ts, py and log fixtures: smart_read (outline, full), recall_file (name, names, no selector, ranges, end clamps, start 0), compress_logs (path, text). Every reply is identical, and so are the tokens.

request full file before after
ts recall L100-130 8144 427 427
rs recall L0-20 (start 0) 10634 379 379
ts recall L1-1000000825 (end far past) 8144 8171 8171
log compress_logs 9200 8408 8408
all 48 356715 97098 97098

A differential test and a property test over every accepted range pin it in the suite. No existing test was edited.

Behavior that changes

Three requests were answered before with something other than what they asked for, and now get the error: a start one to four lines past the end (the last lines, or nothing), a start up to seven lines after the end (a window around lines nobody named), and a range that starts at line 1 or later on an empty file. start_line 0 is unchanged (read from line 1; on an empty file, the empty window). No schema change.

Audit

Every index, slice, arithmetic and unwrap that tool arguments or file contents reach:

  • recall_file (range, names, no selector), smart_read (outline, full, unknown mode, ranked outline), compress_logs (path, text). The range slice is the only panic found.
  • The modules behind them (structure, ranked, compress, repeat, identity, tokenizer, main) and the meter write after the reply: no panic path; write failures are Results filed as meter_write_failed.
  • Odd files (rs, py, ts, tsx, txt): empty, lone newline, only blank lines, no final newline, CRLF, lone CR, multibyte names, BOM, NUL bytes, cut in half, unterminated syntax, one very long multibyte or ASCII line, 400 tiny lines, through every selector and compress_logs: no panic (no_tool_panics_on_an_odd_file and two siblings). A non-UTF-8 file is an error reply; wrong-typed arguments are invalid params.
  • JSON nested 20k and 200k deep in the params (13 sources, 6 request kinds): serde_json's recursion limit makes it a parse error and the next request is answered (one-off probe, not committed).
  • Not catchable by any guard: stack overflow and out-of-memory abort the process. safe_read has no size cap; left as is.

Panic guard

Added, because the audit cannot prove the parser-driven paths panic-free and one panic ends every tool.

  • dispatch_guarded wraps dispatch per request, and main.rs calls it. A panic is answered with -32603 ("the server is still running"), printed on stderr, and filed as a fault, not swallowed.
  • New kind request_panic: tool name, path (capped at 4096 chars), and panicked at file:line:col from a panic hook. The panic message is in the reply and on stderr but not in the fault: it can quote the text being read, and reports hold no file contents (a_fault_for_a_panic_never_carries_the_text_the_call_was_reading). LUMEN_CAPTURE=0 is honored; a missing or unwritable spool says so on stderr.
  • Repeat-read memory is dropped after a panic: the model never received that reply, so a repeat must not be answered as unchanged.
  • Registered in report.rs (FAULT_KINDS, kind_priority rank 1 below a fired fail-open guard, impact, headline) with tests; README and CHANGELOG updated. The doctor has no per-kind code. No allowlist entry: the silent-fallback guard is unchanged and green.
  • Tests inject a panicking closure into guarded, and a differential test shows it changes no answer that did not panic. Also run once on a binary with the range check removed: both panicking calls answered -32603, later calls were answered, two records reached the spool, none with LUMEN_CAPTURE=0.
  • Not covered: the meter write after the reply (audited above), stdout writes (a closed client pipe should end the server), aborts.

Verified

The local gate (fmt, clippy with warnings denied, every crate's tests, frontend, e2e, MSRV build) is green on b10980b. It ran with LUMEN_RANKED_TIME_BUDGET_MS=60000 because the machine's load was above 50; CI runs without it. Run on macOS only.

HackPoint and others added 2 commits October 12, 2026 13:58
…ly, and a panic in one request no longer ends the server

recall_file sliced the source with a start five or more lines past the end of
the file, or far enough after its end_line, and panicked. The server exited
with status 101 and answered nothing after that, so every Lumen tool was gone
for the rest of the session.

A range the file cannot satisfy is now an invalid-params error that states the
file's line count and what was asked, an end past the last line is clamped to
it, and the arithmetic saturates. A range the file can satisfy is answered
byte for byte as before.

Each request now runs under a panic guard. A panic is answered with an
internal error, filed as a request_panic fault (the tool, the path, and the
file and line it panicked at, never the panic message, which can quote the text
being read), and the repeat-read memory of the call is dropped. lumen report
ranks the new kind just below a fired fail-open guard.
@HackPoint HackPoint mentioned this pull request Oct 12, 2026
9 tasks
HackPoint and others added 3 commits October 12, 2026 17:08
…start_line 0 still answers

The changelog said any range on an empty file was refused. A start_line of 0 is
not: it reads from line 1, which on an empty file is the empty window. The
unit test now pins that case next to the refused ones.
@HackPoint
HackPoint merged commit 975436a into main Oct 13, 2026
17 checks passed
@HackPoint
HackPoint deleted the agents/recall-file-range-panic branch October 13, 2026 07:32
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