nisse-review: read long files in parts; a retry starts afresh
From its failed review of #156: read_file stopped at 20,000 characters with no way on, so the reviewer hunted each file's end a definition at a time and ran out of turns; its retry carried every read before it, and a model call timed out. Both drafts were lost. - read_file takes a start line and says where the next part starts. - Each run's view starts at its brief (Transcript.since); the log keeps the rest. - A run out of turns with points drafted posts them as a partial review. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
This commit is contained in:
1 parent
082631eafe
commit
1a7836167d
7 files changed
+141
-12
No files matched your search
@@ -27,9 +27,11 @@ MAX_ISSUES = 20
|
||||
REF = "head (the PR's code) or base (the branch it merges into)."
|
||||
READ_FILE = tool(
|
||||
"read_file",
|
||||
"Read a file in the repository. Its lines are numbered, for comment.",
|
||||
"Read a file in the repository. Its lines are numbered, for comment. A "
|
||||
"long file comes in parts: the end of each says where the next starts.",
|
||||
path="The file's path from the repository root.",
|
||||
ref=REF,
|
||||
start="Optional: the line to start from (1, the default, is the top).",
|
||||
)
|
||||
LIST_FILES = tool(
|
||||
"list_files",
|
||||
@@ -131,11 +133,27 @@ class Repository:
|
||||
lines = self.lines(path, ref)
|
||||
if lines is None:
|
||||
return ToolOutcome(f"{path}: no such file at {ref}", False)
|
||||
text = "\n".join(f"{n:>5} {line}" for n, line in enumerate(lines, 1))
|
||||
if len(text) > MAX_FILE_CHARS:
|
||||
more = len(text) - MAX_FILE_CHARS
|
||||
text = text[:MAX_FILE_CHARS] + f"\n[{more} more characters not shown]"
|
||||
return ToolOutcome(f"[{self.repo}@{ref}: {path}]\n{text}", True)
|
||||
raw = str(arguments.get("start") or "1").strip()
|
||||
if not raw.isdigit() or not 1 <= int(raw) <= max(len(lines), 1):
|
||||
return ToolOutcome(f"start is a line number from 1 to {len(lines)}", False)
|
||||
# A part of at most MAX_FILE_CHARS, in whole lines; the rest is a
|
||||
# read away, never out of reach (#156: a long file's end was).
|
||||
first = int(raw)
|
||||
shown: list[str] = []
|
||||
size = 0
|
||||
for n in range(first, len(lines) + 1):
|
||||
row = f"{n:>5} {lines[n - 1]}"
|
||||
if shown and size + len(row) + 1 > MAX_FILE_CHARS:
|
||||
break
|
||||
shown.append(row[:MAX_FILE_CHARS])
|
||||
size += len(row) + 1
|
||||
last = first + len(shown) - 1
|
||||
where = f"lines {first}-{last} of {len(lines)}"
|
||||
more = (
|
||||
f"\n[{where}: read on with start={last + 1}]" if last < len(lines) else ""
|
||||
)
|
||||
text = "\n".join(shown)
|
||||
return ToolOutcome(f"[{self.repo}@{ref}: {path}, {where}]\n{text}{more}", True)
|
||||
|
||||
def _list(self, arguments: Mapping[str, Any]) -> ToolOutcome:
|
||||
folder = str(arguments.get("path", "")).strip().strip("/")
|
||||
|
||||
@@ -118,6 +118,10 @@ class Reviewer:
|
||||
"""The review run: it ends waiting on post_review, or not."""
|
||||
transcript = self._store.transcript(item_id, "driver")
|
||||
start = transcript.read()[-1].id if transcript.read() else 0
|
||||
# Each run starts afresh from its brief: a retry doesn't carry the
|
||||
# failed run's reads (#156: they filled the context, and a call timed
|
||||
# out). The log keeps them.
|
||||
transcript = transcript.since(start)
|
||||
self._brief(transcript, repo.repo, pr)
|
||||
project = _project(repo.repo)
|
||||
index = self._knowledge.index(item_id, project)
|
||||
|
||||
@@ -38,6 +38,12 @@ from nisse_review.state import (
|
||||
pr_of,
|
||||
)
|
||||
|
||||
PARTIAL = (
|
||||
"A partial review: I ran out of turns before finishing, so these are the "
|
||||
"{n} point(s) I had drafted, without an overall view. A new push gets a "
|
||||
"full look."
|
||||
)
|
||||
|
||||
_WIP = re.compile(r"^\s*(\[wip\]|wip\b)", re.IGNORECASE)
|
||||
|
||||
|
||||
@@ -238,10 +244,18 @@ class Scheduler:
|
||||
outcome, draft = self._reviewer.run(item.id, pr, repo)
|
||||
finally:
|
||||
repo.close()
|
||||
if outcome.kind != "wait":
|
||||
if outcome.kind == "capped" and draft.comments:
|
||||
# Out of turns with points drafted: they're worth more posted
|
||||
# than lost (#156), as a review that says it's partial.
|
||||
self._oplog.emit(
|
||||
"review.partial", cause=outcome.note, work_item=item.id
|
||||
)
|
||||
summary = PARTIAL.format(n=len(draft.comments))
|
||||
elif outcome.kind != "wait":
|
||||
self._failed(item, known, pr, outcome.note or outcome.kind)
|
||||
return
|
||||
summary = str((outcome.arguments or {}).get("summary", ""))
|
||||
else:
|
||||
summary = str((outcome.arguments or {}).get("summary", ""))
|
||||
posted = self._reviewer.post(item.id, known.repo, pr, summary, draft)
|
||||
except GiteaError as exc:
|
||||
self._failed(item, known, None, f"Gitea: {exc}")
|
||||
|
||||
Reference in new issue
Block a user