Files
poimen-memory/tasks/M3.7.4-context-endpoint.md

184 lines
8.3 KiB
Markdown
Raw Permalink 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.
# M3.7.4 — `/memory/context` — three-tier lookup
| Field | Value |
|---|---|
| Phase | M3.7 — Tool context |
| Size | M — 13 days |
| Status | ⬜ Not started |
| Flags | — |
| Spec | inlined below |
| Blocks | M3.7.6 |
| Depends | M3.7.3, M3.7.7, M3.7.8, M3.5.3, M3.6.5 |
## Goal
One call that answers “what do we already know about this failure, tool or task”,
cheapest tier first, and says which tier the answer came from.
## Existing code to build on
**`crates/mem-core/src/lesson.rs`** already implements the tier-1 and tier-2 lookup pattern:
| Function | What it does | Reuse plan |
|---|---|---|
| `lookup(sig, lessons, floor)` | Exact hash match (tier 1) then trigram similarity (tier 2) with abstention floor | **Extend**: add vector search (M2.1) as tier 2, keep trigrams as offline fallback |
| `Tier` enum (`Exact`, `Similar(f32)`) | Tier labeling | **Extend**: add `Reference` variant for tier 3 |
| `Hit` struct | `{ lesson, tier }` | **Extend**: generalize from `Lesson` to `MemoryNode` |
| `similarity(a, b)` | Jaccard over character trigrams | **Keep** as fallback when embeddings unavailable |
| `render_injection(hit, max_chars)` | Capped injection text | **Reuse** for budget management |
**`crates/mem-cli/src/lessons_cmd.rs`** already implements:
| Command | What it does | Reuse plan |
|---|---|---|
| `mem lookup --tool T --file F` | CLI tier-based lookup with floor | **Model for** the HTTP endpoint |
## Files
| Action | Path |
|---|---|
| **Exists** | `crates/mem-core/src/lesson.rs``lookup()`, `Tier`, `Hit`, `similarity()` |
| **Exists** | `crates/mem-cli/src/lessons_cmd.rs``mem lookup` CLI |
| Create | HTTP endpoint in `mem-api` crate (M3.5.1 server) |
| Modify | `crates/mem-core/src/lesson.rs` — extend `Tier` enum with `Reference` variant |
| Create | `tests/it_context_endpoint.rs` — integration tests (12 assertions) |
## Facts (inlined — no spec read needed)
```
POST /memory/context
{ "tool": "github-actions",
"signature_source": "<50KB run log>",
"project": "homelab", "scope": "all-projects", "budget": 6000 }
→ 200 {
"tier": 1,
"lessons": [
{"tier":1,"level":"L1","seen_count":3,"last_seen":"2026-07-02",
"text":"peer dep conflict @types/react 18 vs 19; npm ci --legacy-peer-deps
unblocks, real fix is pinning in overrides",
"parents":[{"level":"L0","source":"pi:…"}]},
{"tier":2,"level":"L2","score":0.71,"matched_kind":"symptom","text":"…"}
],
"skills": [{"name":"ci-triage","score":0.77}],
"budget": {"limit":6000,"used":2140,"dropped":[]}
}
```
Three accepted inputs, any combination: `tool`, `task`, `signature_source`. At
least one is required; `signature_source` without `tool` is allowed and the tool
is inferred by the extractor's rule match.
**Tiers, cheapest first:**
| Tier | Mechanism | Meaning |
|---|---|---|
| 1 | `sig_sha` primary-key hit on `failure_signature` | this exact failure happened here before |
| 2 | vector over `kind='symptom'` then `kind='text'`, reranked | something similar happened |
| 3 | R reference corpus | nobody here has hit this; here are the docs |
Tier 1 does not short-circuit tiers 2 and 3 — it *leads*. An exact hit plus two
related memories is a better answer than an exact hit alone, and the tiers cost
milliseconds relative to the caller's own inference.
**`tier` is in the response and this matters.** The caller, and the human reading
its output, must be able to tell "we hit this exact error in July" from "here is
what the manual says". Presenting tier 3 in the register of tier 1 is how a
retrieval system becomes untrustworthy.
**Ordering is by tier, then rerank, and never by raw score.** A cheatsheet is
written to match the phrasing of a question and will routinely out-score the
terse memory that actually solved it. Precedence is a rule: tier 1 > L1/L2 > R.
**Scope defaults differ by tier.** Signature and symptom lookups federate across
projects — `ERESOLVE` is not homelab-specific — while task-shaped queries stay
project-scoped unless asked otherwise. Project match becomes a rank boost, not a
filter.
**Superseded memories are excluded, not demoted.** A lesson about Kong config is
wrong now, not merely old. M2.4 filters them at the repository; this endpoint
surfaces the successor if one is linked.
**Budget, fixed truncation order:** drop R, then trim tier-2 results toward the
floor, then drop skills. Tier-1 hits are never dropped — they are the smallest and
most valuable content in the response.
**Legs degrade independently.** A skills timeout returns `"skills":[]` with a
`degraded` note and a 200. There is no leg whose failure justifies a 5xx here; a
thinner answer beats no answer when someone is mid-incident.
## Steps
1. Route in the M3.5.1 server; accept `tool`, `task`, `signature_source`.
2. If `signature_source` present, extract and normalise (M3.7.7), then tier 1.
3. Tier 2 concurrently: symptom-vector search, then text-vector, merge, rerank.
4. Tier 3 only if tiers 12 leave budget unfilled.
5. Skills leg (M3.7.3) concurrently with tier 2.
6. Order by tier, apply precedence, apply budget, record drops.
7. On tier-1 hit, append an occurrence record to the log so `seen_count` grows.
8. One structured log line: tiers fired, latencies, scores, drops.
## Acceptance
- A previously-seen failure returns `tier: 1` with `seen_count` ≥ 2.
- An unseen but similar failure returns tier 2 with `matched_kind: "symptom"`.
- A wholly unknown failure returns tier 3 and says so.
- Tier 1 outranks a higher-scoring R result.
- Identical requests return byte-identical bodies apart from the occurrence side
effect.
## Verify
**Harness:** M3.5.1 test server over a store seeded with the poimen log, a
reference corpus, promoted skills, and signatures from replayed real failures.
Fault injection per leg.
**Integration test**`tests/it_context_endpoint.rs`:
1. `a1_tier1_exact` — replay a failure already in `failure_signature`; assert
`tier: 1` and the correct memory.
2. `a2_tier1_counts_occurrence` — call twice; assert `seen_count` incremented and
an occurrence record is in the log.
3. `a3_tier2_symptom` — a novel wording of a known incident returns tier 2 with
`matched_kind: "symptom"`.
4. `a4_tier2_beats_text_only` — same query with symptom vectors deleted ranks the
correct memory lower; assert the symptom path strictly improves it.
5. `a5_tier3_fallback` — an unknown failure returns tier 3 and no lesson claims a
lower tier.
6. `a6_precedence_over_score` — seed an R node that reranks above a tier-1 hit;
assert the tier-1 hit still leads and the response exposes both raw scores.
7. `a7_superseded_excluded` — mark a memory superseded; assert it is absent and
its successor is present.
8. `a8_budget_order` — shrink the budget stepwise; assert drops occur R, then
tier 2, then skills, and that tier 1 is never dropped.
9. `a9_skills_degrade` — inject a skills timeout; assert 200, `[]`, `degraded`.
10. `a10_signature_without_tool` — omit `tool`; assert the extractor infers it.
11. `a11_scope_federation` — a signature seeded under another project is found
with `scope: all-projects` and not with `scope: project`.
12. `a12_used_matches_actual` — tokenize the body independently; assert equality
with `budget.used`.
**Command:** `cargo test -p mem-api context_endpoint`
**False pass:**
- Asserting tier 1 fires without asserting tiers 2 and 3 still populate. A
short-circuit passes assertion 1 and produces a thin answer in exactly the case
where the most context is available.
- Testing precedence on a fixture where the tier-1 hit also scores highest.
Assertion 6 is only meaningful when score and tier disagree.
- Testing degradation with an empty leg instead of a failing one. Timeouts take
the path that 500s in production.
## Traps
- Running tier 3 unconditionally. Reference chunks are long, they fill the budget,
and they push real memories out of a response that had better answers available.
- Incrementing `seen_count` on a retry. The orchestrator retries activities; an
occurrence should key on the caller's request id or the count inflates and
`last_seen` stops meaning anything.
- Returning tier as a label the caller has to interpret from ordering. It is a
field; if it is implicit, every consumer reimplements the inference differently.
---
Background: [DESIGN.md](../DESIGN.md) — tool context, retrieval tiers