Skip to content

Auto-search hints: up to 3 s on every user turn, and the hints are mostly unusable #743

Closed

Description

@null-axiom

Magic Context: 0.49.13 measured locally; code references at v0.49.15. OpenCode 2, macOS, Voyage voyage-4-lite via openai-compatible, auto_search defaults.

What we see

  • pp.autoSearchHint in the transform log, 840 runs: p50 0.2 ms, p90 1,362 ms, max 3.3 s, 286 s total. It is the main source of slow transforms in our two long sessions (lead: 555 ms average per pass, 180 passes over 500 ms).
  • From session_meta.auto_search_hint_decisions (all sessions): 555 decisions, 169 hints. Of 260 fragments, 166 (64%) are truncated first lines and 36 (14%) start with raw <task id=… / <task_cohort …> XML, e.g.
- <task id="ses_edbb8f9afffeJ5bcT83o7BTStz" state="completed"> <summary>Backgroun…

The agent can't judge relevance from these, so it tends to ignore hints, including ones that would have answered the question.

Causes in the code

  1. A new worker per turn. hooks/magic-context/auto-search-worker-client.ts:352 creates a Worker for each search, and :250-306 queues a second one for backfill. Each worker opens SQLite and rebuilds the decoded compartment-embedding pool (the cache in compartment-chunk-embedding.ts:1192-1222 is per process), which here is ~2,200 compartments, before the query embedding returns. The wait is capped by AUTO_SEARCH_TIMEOUT_MS = 3000 (auto-search-deadline.ts:11), awaited in the transform (transform-postprocess-phase.ts:4033), matching our 3.3 s max. We did not measure the split between decode time and the Voyage round trip.
  2. Scores are ranks, not similarity. mergeMessageAndCompartmentResults replaces every score with linearDecayScore(rank, n) (features/magic-context/search.ts:1863-1866), so the top merged hit always scores 1.0 and clears score_threshold: 0.6. (We still log 75 below-threshold decisions, so some path filters; but the threshold isn't measuring relevance.)
  3. Hints can quote the live context. ctx_search passes maxMessageOrdinal = lastCompartmentEnd (tools/ctx-search/tools.ts:129-130,209); runAutoSearchHint passes none (auto-search-runner.ts:342-370), so hits can come from messages the model already sees, possibly the current prompt itself.
  4. Fragments are the start of the message, not the match. renderFragment (auto-search-hint.ts:57-113) previews the first characters of row.content. getIndexableContent (message-index.ts:584-606) indexes task notifications with their XML, and strips reminders with the non-greedy regex the runner's own comment (auto-search-runner.ts:106-122) says is wrong for nested reminders.

Suggested fixes

  • Keep one long-lived search worker (or cache the decoded pool in the owner); lower the deadline to ~1 s and fall back to an FTS-only hint when the vector is late.
  • Carry the real cosine/BM25 score through the merge and apply the threshold to it.
  • Pass the same live-tail cutoff as ctx_search, and exclude the current user message.
  • For message hits use the matched snippet (FTS snippet() or the chunk text); for compartments use the compartment title. Skip or strip task notifications at index time.

Activity

  1. magic-alfonso commented on Oct 20, 2026

    @magic-alfonso

    Thanks, this is a clear report. We're investigating each point: the per-turn latency and its 3 s deadline, the merged scores that make the threshold ineffective, the missing live-tail cutoff, and fragments that show truncated first lines or raw task XML. I'll follow up here with the findings and the fix.

  2. magic-alfonso commented on Oct 22, 2026

    @magic-alfonso

    Fixed in 0.49.17. Thanks for the measurements; they made each part easy to check.

    • Latency: the hint search now waits at most 200 ms for a query embedding and falls back to keyword search when it's slower. The whole search has a 1-second deadline instead of 3, and search workers are reused. On our Linux runs, p95 went from about 835 ms to about 222 ms using real embedding-provider timings.
    • Scores: merged results keep their real relevance scores instead of rank positions, so the threshold filters again.
    • Already-visible content: the live tail and the current message are excluded.
    • Fragments: a hint shows the matched passage instead of a truncated first line.

    Hints already shown in a session replay byte-identically, so updating doesn't rewrite any cached prefix.

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

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions