Files
poimen-memory/tasks/M8.6-query-endpoint-upgrade.md
T
Story Crater Bot 959c596b1d chore: Archive completed task files (M0, M1, M3, M3.5, M4.1-2, M3.6.1)
Deleted 31 completed task files:
- M0.x: 8 tasks (cargo, domain types, recordsource, tokenizer, adapters, gate)
- M1.x: 8 tasks (llm-chat, standing-query, prompt template, parser, loop, log, e2e, gate)
- M3.x: 4 tasks (l2-synthesis, rerank, mem-query, gate)
- M3.5.x: 8 tasks (http-server, ingest, query, federation, skills, projects, rate-limiting, gate)
- M3.6.1: DocCorpusSource (heading-boundary chunking)
- M4.1-2: skill-draft, derived-filter

Updated INDEX.md:
- Removed M0 & M1 phase sections (archived in git history)
- Updated progress table: 65 active tasks (42 + 2🟡 + 21)
- Updated status: M0/M1 complete, M3/M3.5 gates passing, M4.1-2 done
- Noted M3.5.10 JWT auth implementation complete (awaiting image rollout)
- Cleaned up broken links to deleted task files

Total test count: 239 passing, 2 ignored (up from 196 at M3.4)
Ready for M4.3 gate composition, M5 post-training, M7 source connectors.
2026-08-27 20:25:05 -07:00

128 lines
4.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# M8.6 — Upgrade GET /memory/query to hybrid with fallback
| Field | Value |
|---|---|
| Phase | M8 — Hybrid Search |
| Size | M — 12 days |
| Status | ⬜ |
| Flags | — |
| Spec | inlined below |
| Blocks | M8.8, M8.9 |
| Depends | M8.5 (hybrid query worker compiles and passes tests) |
## Goal
Replace the `QueryWorker` call in `GET /memory/query` with `HybridQueryWorker`. Add `?method=` parameter for explicit strategy override. Implement fallback chain: hybrid → semantic → error.
## Design
**Updated request:**
```
GET /memory/query?query=...&project=...&limit=10&method=hybrid
Authorization: Bearer <JWT>
```
New parameter:
- `method` (optional) — `hybrid` (default), `semantic`, `lexical`. If omitted, `QueryOptimizer` decides.
**Handler logic:**
```rust
async fn query_handler(...) -> HttpResponse {
let (claims, token) = validate_auth(...)?;
check_capability(&claims, "memory:read")?;
check_rate_limit(&claims, &state, "/memory/query")?;
let method_override = query.get("method").map(|m| match m.as_str() {
"semantic" => SearchStrategy::SemanticOnly,
"lexical" => SearchStrategy::LexicalOnly,
_ => SearchStrategy::Hybrid, // includes "hybrid" and unknown values
});
// Try hybrid worker first
if let Some(ref hybrid) = state.hybrid_query_worker {
match hybrid.query(&project, &question, limit, &token, method_override).await {
Ok(response) => return HttpResponse::Ok().json(response),
Err(e) => {
tracing::warn!("hybrid query failed, falling back: {}", e);
}
}
}
// Fallback: existing semantic-only worker
match state.query_worker.query(&project, &question, Some(limit)).await {
Ok(results) => HttpResponse::Ok().json(json!({
"query": question,
"project": project,
"search_strategy": "semantic_fallback",
"results": results,
})),
Err(e) => HttpResponse::InternalServerError().json(json!({"error": "query_failed"})),
}
}
```
**AppState change:**
```rust
pub struct AppState {
// ... existing fields ...
pub hybrid_query_worker: Option<Arc<HybridQueryWorker>>, // None if OpenSearch not configured
}
```
Constructed in `start_server()`:
```rust
let hybrid = if std::env::var("OPENSEARCH_HOSTS").is_ok() {
let os_client = OpenSearchClient::new(hosts);
Some(Arc::new(HybridQueryWorker::new(vector_store, embeddings, Some(Arc::new(os_client)))))
} else {
// No OpenSearch configured — hybrid worker without lexical
Some(Arc::new(HybridQueryWorker::new(vector_store, embeddings, None)))
};
```
**Backward compatibility:** If `OPENSEARCH_HOSTS` is not set, the worker still works but always uses `SemanticOnly`. Existing clients see the same results with an added `search_strategy` field.
## Steps
1. Add `hybrid_query_worker` field to `AppState`.
2. Construct `HybridQueryWorker` in `start_server()`, gated on `OPENSEARCH_HOSTS` env.
3. Update `query_handler()` with method override + fallback chain.
4. Add `?method=` query parameter parsing.
5. Update response format to include `search_strategy` and `metrics`.
6. Write integration test: query with `method=hybrid`, verify response has `metrics`.
7. Write integration test: query without OpenSearch, verify fallback to semantic.
## Acceptance
1. `GET /memory/query?query=test&project=p` returns `"search_strategy": "Hybrid"` when OpenSearch configured.
2. `GET /memory/query?query=test&project=p&method=semantic` forces semantic-only, response says `"search_strategy": "SemanticOnly"`.
3. If OpenSearch is down, query still succeeds with `"search_strategy": "semantic_fallback"`.
4. Response includes `metrics` block with timing data.
5. Existing clients that don't send `method` param get the same results as before (backward compat).
6. JWT token is forwarded to OpenSearch (not a new token, not admin credentials).
## Verify
```bash
# With OpenSearch running
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/memory/query?query=kubernetes+port&project=poimen" | jq .search_strategy
# Should output: "Hybrid"
# Force semantic
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/memory/query?query=kubernetes+port&project=poimen&method=semantic" | jq .search_strategy
# Should output: "SemanticOnly"
# Kill OpenSearch, retry
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/memory/query?query=kubernetes+port&project=poimen" | jq .search_strategy
# Should output: "semantic_fallback"
```
**False pass:** Handler catches the hybrid error silently and always falls back to semantic — user never sees hybrid results even when OpenSearch is healthy. Assert that with OpenSearch up, `metrics.lexical_candidates` is `Some(n)` where `n > 0`.
## Artifacts
- Modified `crates/mem-cli/src/http_server.rs`