Skip rebuilding an ActionResult for a research-cache lookup - #170
Merged
Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B46vQvvc47LJeCmqQxAhme
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements
plans/029-cached-research-cheap-path.md. Three lines, no behavior change.The problem
Release 0.4.0 (#158, region states) changed
Researcher.getCachedResearchfrom a plain hash lookup into:Keying by the region-less base hash was necessary —
state.hashnow forks on regions. ButActionResult.fromStatesynchronously reads up to three files from disk (page HTML, browser log, and the full screenshot PNG buffer), andbaseHashthen runs a parse5 parse of the whole page to extract headings.So a cache lookup became one of the most expensive calls on the prompt-build path. Worse, two agents pay it twice per invocation, because they already hold the very
ActionResultbeing rebuilt:captain.ts:155buildsconst actionResult = ActionResult.fromState(state), then:163rebuilds itnavigator.ts:577buildscurrentActionResult, then:578rebuilds itThe change
An early exit in the static method, and the two call sites pass what they already hold:
The signature is unchanged.
ActionResult implements ActionResultDataandActionResultData extends WebPageState, so anActionResultalready IS aWebPageState— aWebPageState | ActionResultunion would be redundant, andinstanceofnarrows the parameter on the cheap path.Call sites holding only a
WebPageState(researcher.ts:662,freesail-command.ts:46) keep today's behavior unchanged.Cache-key equivalence
The cheap path skips
fromState'surl: state.fullUrl || state.urlnormalization, so the key had to be proven identical or cached research would silently re-run.computeStateHashkeys offrelativeUrl || url, and the constructor already appliesextractStatePath, which is idempotent for path-like input.Verified empirically —
ar.baseHash === ActionResult.fromState(ar).baseHashacross four URL shapes:fullUrlprojects_abc_suites_h1_suites_h2_listprojects_abc_plans_tab_x_h1_plans#hashdash_section_h1_dashfullUrllogin_h1_loginIdentical on both paths in every case.
Verification
bun test tests/unit— 1225 pass / 0 failbun test tests/integration/researcher.test.ts— 10 pass / 0 failbun run format/bun run lint— exit 0grep -n 'getCachedResearch(state)' src/ai/captain.ts src/ai/navigator.ts— no matchesBoth switched arguments were confirmed to be built from the same state as before: captain's
stateis not reassigned between:151and:163; navigator's optionalactionResultparameter is never supplied by its single caller (freesail-command.ts:67), socurrentActionResultis alwaysActionResult.fromState(state)today.Notes for the reviewer
ActionResult.fromStateis deliberately untouched. Adding anif (state instanceof ActionResult) return state;early exit there would give every caller the cheap path, but it would also skip that method'surl: state.fullUrl || state.urlnormalization — a real behavior change, deferred by the plan.ActionResult, and the plan says skip rather than create one.tsc --noEmiterrors (captain.ts:35,navigator.ts:706) are unrelated to the edited lines and predate this change; the repo gates on Biome plus tests.CHANGELOG.mdentry — the plan scopes it out and there is no user-visible behavior change. Happy to add one.🤖 Generated with Claude Code
https://claude.ai/code/session_01B46vQvvc47LJeCmqQxAhme