feat(groomer): give repository exploration its own budget - #918
Conversation
Exploration borrowed maxContextBytes and timeoutMs from the single grooming call. Those were sized for one pre-computed context and one request, not a multi-turn tool loop, so the loop exhausted its budget partway through an investigation and stopped early — reporting fewer files than it had found. Size it three ways, most specific first: the individual DISPATCH_GROOMER_EXPLORE_* overrides, DISPATCH_GROOMER_MODEL_CONTEXT_TOKENS deriving from the model's real window, or DISPATCH_GROOMER_CONTEXT_MODE (small|medium|large). The resolved budget and which path produced it are recorded on every run under contextSummary.exploration.budget. Claude-Session: https://claude.ai/code/session_01YSuDvZq9ncvyX85Uzx3cQh
There was a problem hiding this comment.
AI Automated Review
Full PR review.
Analysis engine: MiniMax-M2.7@https://litellm.jory.dev/v1 (anthropic) — primary route
Recommendation: Approve
This PR introduces a dedicated exploration budget for the groomer's repository exploration tool loop. The code is well-structured, well-tested, and solves a documented real-world problem (86 budget warnings and 26 timeouts across 62 runs).
Change-by-Change Findings
New: src/lib/groomer/exploration-budget.ts
Introduces resolveExplorationBudget() and deriveFromContextTokens(). Resolution follows a clear priority: individual env overrides → model context tokens derivation → named mode (small/medium/large). The code includes defensive guards: maxFileBytes is capped at maxTotalBytes regardless of how the budget was set, and deriveFromContextTokens applies a floor of 4,096 bytes for tiny windows.
New: src/lib/groomer/exploration-budget.test.ts
18 new tests covering: defaults, named modes, monotonic growth across modes, fallback on unrecognized mode, context-token derivation (both large and small windows), the per-file cap safety, individual overrides, and invalid/non-numeric input handling. Tests are thorough and deterministic.
Modified: src/lib/groomer/config.ts
Wires ExplorationBudget into HostedGroomerConfig and calls resolveExplorationBudget() to populate it.
Modified: src/lib/groomer/run.ts
Switches the exploration loop from config.timeoutMs/config.maxContextBytes/config.maxFileBytes to config.exploration.*. Also records the resolved budget in contextSummary.exploration.budget for observability.
Documentation: README.md, docs/hosted-groomer.md
New environment variables (DISPATCH_GROOMER_CONTEXT_MODE, DISPATCH_GROOMER_MODEL_CONTEXT_TOKENS, DISPATCH_GROOMER_EXPLORE_MAX_BYTES, DISPATCH_GROOMER_EXPLORE_MAX_FILE_BYTES, DISPATCH_GROOMER_EXPLORE_TIMEOUT_MS) are documented with a sizing table. Includes rationale for preferring DISPATCH_GROOMER_MODEL_CONTEXT_TOKENS for self-hosted models.
Standards Compliance
The AGENTS.md standards file does not prescribe conventions specific to budget configuration or groomer internals. The PR follows general patterns observed in the codebase:
- New env vars documented in README and docs/hosted-groomer.md — consistent with other
DISPATCH_GROOMER_*variables. - Default mode is "medium" — aligned with the PR's stated goal of suiting "a modest self-hosted model."
- Observability via
contextSummary.exploration.budget— consistent with the existingcontextSummarypattern. - No commit of secrets — no secrets or
.envchanges. - TypeScript clean — confirmed by
npx tsc --noEmitpassing.
Required Check: Path Traversal Vulnerabilities
Verified not applicable. This PR operates on budget configuration (byte limits, timeouts, mode strings) and does not handle filesystem paths. The tool loop that reads files from the repository lives in a separate module (repository-context.ts), not modified by this PR. The new ExplorationBudget interface only describes numeric limits and a source discriminator — no path strings are constructed or traversed.
Required Check: Edge-Case Paths (null bytes, symlinks)
Verified not applicable. The exploration-budget.ts module has no filesystem interaction. It parses environment variable strings into numbers (parseIntEnv) and mode strings (parseMode), neither of which are used to open or read files. Null-byte injection and symlink traversal are concerns for the file-reading layer, which is outside this PR's scope.
Tool Harness Findings
No native tool calls were issued. The harness found no issues to report.
CI Results
All 11 CI checks passed (Lint, Tests, Database migrations, Build, smoke, Docker Build (MCP), Typecheck, Database integration, Coverage, npm audit, Docker Build).
Unknowns / Needs Verification
None. The PR body includes self-verification results (vitest, tsc, lint, build), and the CI confirms all checks pass. Documentation is complete and consistent.
Summary
maxContextBytesandtimeoutMsfrom the single grooming call.DISPATCH_GROOMER_EXPLORE_*overrides,DISPATCH_GROOMER_MODEL_CONTEXT_TOKENS(derived from the model's real window), orDISPATCH_GROOMER_CONTEXT_MODE=small/medium/large.Why
Exploration is a multi-turn tool loop but was budgeted like a single call. With
the borrowed defaults the loop got 8 KB total and 4 KB per file, so two file
reads exhausted it. Across 62 runs on one deployment that produced 86
repository exploration hit its byte budgetwarnings and 26 timeouts, with theloop stopping at ~6.8 of its 12 permitted tool calls — it was running out of
bytes, not turns, and reporting fewer files than it had found.
Sizing
smallmedium(default)largemediumis aimed at a modest self-hosted model rather than any particulardeployment. Operators with a larger window should prefer
DISPATCH_GROOMER_MODEL_CONTEXT_TOKENSover guessing a preset — it reserves65% of the window for the system prompt, issue context and output, and derives
the rest. That mirrors
model_context_tokensinpr-reviewer-action, whichexists for the same reason: named presets assume windows that self-hosted
models do not have.
A single file can never exceed the total budget, however the two are set.
Observability
contextSummary.exploration.budgetnow records the resolved values and whichof the three paths produced them, so a starved run can be diagnosed from the
record rather than inferred from warnings.
Verification
npx vitest run— 2480 passed, 5 skipped (18 new)npx tsc --noEmit— cleannpm run lint— 0 errorsNODE_ENV=development npm run build— succeedshttps://claude.ai/code/session_01YSuDvZq9ncvyX85Uzx3cQh