Problem
Cold semantic index builds for remote embedding backends are far too slow for medium/large repos, and an interrupted build loses all progress.
1. Embedding batches are strictly serial
build_from_chunks (crates/aft/src/semantic_index.rs:2555-2572) calls the sync embed_fn(batch_texts) one batch at a time — no in-flight concurrency, and the FnMut signature rules out pipelining without a change.
Measured in the wild (OpenRouter, qwen/qwen3-embedding-8b, batch=64): ~5 chunks/s over a 34,736-chunk repo (543 batches), i.e. a ~2 hour cold build. At this batch size the bottleneck is per-request RTT, not server compute — exactly what concurrency would absorb.
There is also no config knob: the semantic config surface is only backend / model / base_url / timeout_ms / query_timeout_ms / max_batch_size / max_files (crates/aft/src/config.rs:134-158). When the upstream provider caps batch size (e.g. DashScope-backed routes reject input arrays > 20), max_batch_size: 20 multiplies the request count ~3x and makes the serial latency problem worse.
2. No mid-build persistence
semantic.bin is written only when the build completes. A cold build killed halfway (user closes the harness, machine restarts) restarts from zero — the 2-hour build has no checkpoint.
Proposal
- Add
semantic.max_concurrent_requests (default 1 = current behavior) and pipeline N batches in flight through the embed engine.
- Periodically persist partial build progress (per-batch checkpoint keyed by the existing fingerprint), so an interrupted cold build resumes from the last flush instead of restarting.
- Optionally: a small request coalescer/buffer so a batch-size clamp from provider errors doesn't inflate request count.
Happy to take a stab at a PR if the overall approach sounds right.
Problem
Cold semantic index builds for remote embedding backends are far too slow for medium/large repos, and an interrupted build loses all progress.
1. Embedding batches are strictly serial
build_from_chunks(crates/aft/src/semantic_index.rs:2555-2572) calls the syncembed_fn(batch_texts)one batch at a time — no in-flight concurrency, and theFnMutsignature rules out pipelining without a change.Measured in the wild (OpenRouter,
qwen/qwen3-embedding-8b, batch=64): ~5 chunks/s over a 34,736-chunk repo (543 batches), i.e. a ~2 hour cold build. At this batch size the bottleneck is per-request RTT, not server compute — exactly what concurrency would absorb.There is also no config knob: the
semanticconfig surface is onlybackend / model / base_url / timeout_ms / query_timeout_ms / max_batch_size / max_files(crates/aft/src/config.rs:134-158). When the upstream provider caps batch size (e.g. DashScope-backed routes reject input arrays > 20),max_batch_size: 20multiplies the request count ~3x and makes the serial latency problem worse.2. No mid-build persistence
semantic.binis written only when the build completes. A cold build killed halfway (user closes the harness, machine restarts) restarts from zero — the 2-hour build has no checkpoint.Proposal
semantic.max_concurrent_requests(default 1 = current behavior) and pipeline N batches in flight through the embed engine.Happy to take a stab at a PR if the overall approach sounds right.