You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Auto-search hints never complete in large sessions: the chunk vector pool is rebuilt cold every turn and the worker is discarded on timeout #762
Magic Context: 0.49.19 (OpenCode 2.1.1, macOS, Bun 1.4.2 as embedded by OpenCode). Embeddings: Voyage. memory.enabled: false.
What happens
In two long sessions (about 20.5K stored chunk embeddings each, ~85 MB), nearly every auto-search attempt hits the 1 s deadline:
Over ~2 h after upgrading: 18 timeouts at exactly 1000 ms (pp.autoSearchHint 1002–1020 ms), 7 completions (2 hints attached, 5 below threshold).
Each timeout adds ~1 s to the turn's transform (postTransformPhase ≈ 1,060 ms) and delivers no hint.
This is not new in 0.49.19: AUTO_SEARCH_TIMEOUT_MS = 1_000 and AUTO_SEARCH_VECTOR_TIMEOUT_MS = 200 are the same in 0.49.17–0.49.19.
Cause (source-traced, with a read-only bench against the real DB)
When the query vector arrives within the 200 ms vector budget, searchCompartmentChunks (features/magic-context/search.ts) calls loadCompartmentChunkEmbeddingsForSearch → refreshDecodedSearchPool (compartment-chunk-embedding.ts). That probes every chunk row in scope, reads them 16 at a time with a setImmediate yield per batch, decodes each to a Float32Array, then probes the whole scope again.
Cold pool build measured at 412 ms in one session and 1,563 ms in the other (probe alone 1,246 ms). The FTS message lane takes another 150–550 ms cold.
On timeout, finish() in auto-search-worker-client.ts disposes the worker, so the decoded pool is thrown away. The next turn starts cold again; a rebuild longer than ~700 ms can never finish, so the pool never warms.
Turns that do complete look like the case where the vector missed its 200 ms budget and the chunk lane returned [] early (inferred; Voyage latency isn't logged).
Scaling follows the number of stored chunk embeddings, not message count.
Expected
Hints either complete or are skipped quickly in large sessions, without a ~1 s stall per turn.
Suggestions
Don't dispose the worker (and its decoded pool) when the deadline fires; let it finish and keep the pool warm for the next turn.
Build or refresh the pool outside the turn (after compartment publish, or on idle), and skip the chunk lane when the pool isn't warm yet.
Log per-step timings when the deadline fires (pool probe/read/decode, FTS, vector wait), and consider logging getAutoSearchWorkerStats().
Optionally make the deadline configurable.
Related, smaller
The "attached hint … top score 1.000" log line prints results[0].score, which for message hits is linearDecayScore(rank, n) — always 1.0 for rank 0. The gate correctly uses relevanceScore, so this is only misleading logging. Survivors are still ordered by the rank score rather than relevanceScore.
queueAutoSearchBackfill runs after every completed search and spawns a worker that opens a writable connection and idles for 5 min, but with memory.enabled: false it does nothing (auto-search-worker.ts requires features.memoryEnabled). The runner could check that before queuing.
Explicit ctx_search builds the same chunk pool on the serving thread (only the message-FTS lane runs in a worker) and passes no abort signal.
Thanks, @null-axiom. This is a precise trace, and the cause checks out on our side: the deadline disposes the worker together with its decoded pool, so a pool that takes more than about 700 ms to build never gets warm.
A fix is in progress. A deadline will only stop waiting and keep the worker and its pool. The pool will be kept warm outside the turn, and a turn whose pool isn't warm yet will skip the chunk lane instead of stalling. It also covers the smaller points you listed. We'll post here when it ships.
Fixed in 0.49.20. Thanks for the report and the detailed trace.
The background search no longer throws away its loaded vectors when a turn's one-second deadline passes, so the next turn starts warm and only loads vectors that are new or changed. A turn whose vectors aren't ready yet skips that part of the search instead of waiting. On a synthetic session with about 50,000 stored chunks, the slowest 5% of warm turns went from over a second, with regular deadline misses, to about 220 ms with none.
Hints also list fragments by relevance, sessions with memory disabled skip the background memory work, and a cancelled ctx_search stops loading history vectors.
Activity
magic-alfonso commentedon Oct 22, 2026
Thanks, @null-axiom. This is a precise trace, and the cause checks out on our side: the deadline disposes the worker together with its decoded pool, so a pool that takes more than about 700 ms to build never gets warm.
A fix is in progress. A deadline will only stop waiting and keep the worker and its pool. The pool will be kept warm outside the turn, and a turn whose pool isn't warm yet will skip the chunk lane instead of stalling. It also covers the smaller points you listed. We'll post here when it ships.
magic-alfonso commentedon Oct 23, 2026
Fixed in 0.49.20. Thanks for the report and the detailed trace.
The background search no longer throws away its loaded vectors when a turn's one-second deadline passes, so the next turn starts warm and only loads vectors that are new or changed. A turn whose vectors aren't ready yet skips that part of the search instead of waiting. On a synthetic session with about 50,000 stored chunks, the slowest 5% of warm turns went from over a second, with regular deadline misses, to about 220 ms with none.
Hints also list fragments by relevance, sessions with memory disabled skip the background memory work, and a cancelled
ctx_searchstops loading history vectors.