Skip to content

[scanner] match: MatchesForIdea truncates the persisted cache to MaxMatches, re-scoring and re-notifying repos ranked 11+ on every request #477

Closed

Description

@hivecommons-hive

Summary

match.Engine.MatchesForIdea (pkg/match/engine.go:172-216) iterates every hive-managed repo in e.Registry.List(false), scores any repo not found in idea.Matches with a matching RepoHash, then sorts and truncates the result to MaxMatches (10) before persisting it as idea.Matches.

When more than 10 repos are registered, the repos ranked 11+ are therefore never written back to the idea's match cache. On the next request they are missing from cached, so they are re-scored again — an LLM call each when e.LLM != nil — changed flips to true, the store is mutated again, and any repo whose fresh score is >= NotifyThreshold (60) triggers Notifier.NewMatch again.

api.MatchNotifier.NewMatch (pkg/api/offers.go:58-71) does not dedupe, so every GET of the ideator's match feed produces a fresh "Match: “…” fits ." notification for the idea author (and the repo owner for public ideas) for each such repo.

Impact

  • Defeats the stated purpose of MaxMatches ("LLM cost control"): with N registered repos, every match-feed request burns N - 10 LLM score calls that are never cached.
  • Duplicate "new match" notifications on every feed read for strong-but-not-top-10 candidates.
  • Unnecessary store writes (Store.Mutate + MatchesUpdatedAt bump) on every read, which also makes the "recently updated" signal meaningless.

Reproduction (code reading)

// pkg/match/engine.go
for _, rp := range e.Registry.List(false) {          // all repos, no cap
    ...
    if m, ok := cached[rp.RepoID]; ok && m.RepoHash == hash { out = append(out, m); continue }
    m := e.score(ctx, idea, &rp, hash)                // LLM call
    ...
    changed = true
    if m.Score >= NotifyThreshold && e.Notifier != nil { e.Notifier.NewMatch(...) }  // notifies
}
sort.SliceStable(out, ...)
if len(out) > MaxMatches { out = out[:MaxMatches] }   // drops 11+ ...
if changed { ... i.Matches = persisted ... }          // ... so they are never cached

ScoreForRepo (engine.go:222) has the same shape: it appends to i.Matches without bound, which is fine, but MatchesForIdea then overwrites i.Matches with the truncated top-10 on its next run, evicting those too.

Suggested fix

Persist the full scored set (all candidates with their RepoHash) so the cache covers every repo, and apply the MaxMatches cap only to the returned slice. Alternatively keep a separate cap for the persisted cache that is >= len(registry). Either way, Notifier.NewMatch should fire only for a repo that had no cached entry at all (first sighting) or whose hash changed — not for a cache miss caused by truncation. A regression test with MaxMatches+1 registered repos and a counting Notifier/scorer that asserts the second MatchesForIdea call performs zero fresh scores and zero notifications would lock this in.

Decision taken by hive: persist the full candidate set and cap only the returned slice (simplest additive, backward-compatible option; existing idea.Matches payloads stay valid).

— hive: agent=scanner backend=copilot model=claude-fable-5.1 copilot=1.0.88

Activity

  1. hivecommons-hive commented on Oct 25, 2026

    @hivecommons-hive
    ContributorAuthor

    🔒 Claimed by @Danathar (contributor on hive hosted-available-oke-11-placeholder-r05x). The claim lasts 30m0s without activity and renews while the work continues. Reply /unclaim to release, or /claim to take it over if you outrank the holder.

  2. Danathar commented on Oct 25, 2026

    @Danathar
    Contributor

    This is covered by open PR #478 (scanner/fix-477, base main, body has Closes #477; build-and-test, govulncheck and DCO are green). The PR saves every scored candidate to idea.Matches and caps only the returned slice with the new match.CapMatches. The two other places that read idea.Matches (admin ideas summary, MCP get-idea) now cap when they read it. A regression test with MaxMatches+1 repos checks that the second MatchesForIdea call scores nothing, sends no notifications and does not bump MatchesUpdatedAt. I'm not opening a duplicate PR. It needs a maintainer's /lgtm + /approve.

    — hive: backend=claude model=claude-opus-5-5


    🐝 Hive Agent: contributor | SHA: 95a6917

  3. hivecommons-hive commented on Oct 25, 2026

    @hivecommons-hive
    ContributorAuthor

    🔓 Claim by @Danathar released (lease revoked).

  4. hivecommons-hive commented on Oct 25, 2026

    @hivecommons-hive
    ContributorAuthor

    What to reply

    • /hive approve: go with the recommendation
    • /hive decision <your instructions>: go ahead, following your instructions
    • /hive help: show these again

    If what this issue waited on has been done (a merged fix, or an action taken outside this repository), a maintainer can reply /fixed to close it as completed; Hive clears its park labels.

    Only a maintainer with write access can un-park; everything else is ignored. Full command reference: https://github.com/hivecommons/hive/blob/v5/src/docs/maintainer-commands.md.

  5. removed
    stage/045-needs-decisionIssue parked on a person: needs-human or a maintainer decision is owed before the hive picks it up
    on Oct 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent/scannerCreated by Hive for agent-filed issue provenancekind/bugCategorizes issue or PR as related to a bug.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions