Skip to content

Spec: Memory Context Endpoint — Planner Output Without Generation

File: .ai/specs/core-engine/2026-07-06-memory-context.md Status: Approved (2026-07-06, J. Porter — all OQs resolved in v0.2; ratified with the Phase-3d set once the memory-cards OQ-1 security countersign cleared the last gate. Dedicated context_* telemetry namespace; no-LLM/no-egress guarantee; one-planner-two-surfaces parity) Owners: Core lane; reviewer: J. Porter Related: PR-089 (this spec), memory-planner spec (PR-050 — the planner IS the mechanism; this spec only adds a surface), generation-orchestration spec (request-shape parity with /v1/generate's planning inputs), temporal-clarification spec (§3.9 pendingClarification caveats reused as openQuestions), mcp-tools spec (memoryContext tool addendum), query-language spec (structured leg + ceilings), gdpr-erasure spec (context traces are already covered by the GenerationTrace/trace rows in the surface register)


1. Overview

Exposes the PR-050 memory planner as a first-class API: POST /v1/memory/context returns the compact, budgeted, scored, permission-scoped evidence set that /v1/generate would plan for the same input — without calling any LLM. Agents and applications ask Telha "what should I remember before I act?" and receive span-level memory items with sources, temporal coordinates, scores, pending-clarification open questions, and a persisted trace. This is the Memory Gateway's memory.context verb; it turns Telha from an answer system into memory infrastructure that other systems compose.

2. Problem Statement

Agents frequently need Telha's memory but not Telha's generation: a sales copilot drafting an email wants evidence-backed context injected into ITS OWN prompt; a workflow engine wants facts + caveats, not prose. Today the planner's output is reachable only through /v1/generate, which (a) forces an LLM round-trip the caller doesn't want, paying latency and the tenant's cost cap for tokens that get thrown away; (b) requires [llm] credentials to be configured at all — a pure memory-backend deployment cannot exist; (c) returns a draft, not structured items, so callers scrape prose to recover what the planner knew precisely (span coordinates, scores, stranded budget); (d) hides the open-questions signal — pending clarifications attach to /v1/query results but not to any evidence-selection surface. The machinery exists and is tested; the gap is purely that no wire surface exposes a plan as the product.

3. Proposed Solution

Normative decisions:

  1. One planner, two surfaces. /v1/memory/context and /v1/generate MUST call the same planner entry point with the same recall legs (structured query ∪ vector ∪ traversal), dedup, scoring, per-source cap, and budget accounting. Divergence between "the context Telha gives you" and "the context Telha uses itself" is a bug by definition (§12 parity test). No context-specific selection logic may exist.
  2. This endpoint never generates and never egresses. No [llm] configuration is required or consulted. The only embedding use is the existing semantic-leg embedding of the question (the same AppState.resolve_vector_text path /v1/generate uses); with no embedding config, the endpoint still works on the structured + traversal legs (recorded in the response stats, not an error).
  3. Request mirrors /v1/generate's planning inputs: question (for the semantic leg), optional structured find/where leg, temporal window (validTime/txTime, defaulting to now — full bitemporal addressing: "context as we knew it then" is expressible), budget {maxTokens?, maxSpans?} clamped by server ceilings, and include {openQuestions, scores, stats} flags. Budget semantics are the memory-planner spec's (B = min(requested, configured, model-independent ceiling); token estimation per its approved estimator).
  4. Response items are span-grade, not prose: each item carries the span text (the plan already carries it), {sourceId, sourceVersionTx, start, end}, source display name, valid-time interval of the backing version, the leg-fused score, and a pendingClarification ref when the backing node has one. No summary field in v1 — a natural-language summary requires generation and belongs to /v1/generate; shipping one here would silently reintroduce LLM dependence and cost. (Deviation from the product plan's example response, deliberate.)
  5. openQuestions reuse the clarification caveat mechanism (temporal-clarification §3.9): pending or conflicted clarifications on any node backing a returned span surface as {clarificationId, state, question} — the agent-facing twin of the human card.
  6. Every context call persists a trace (traces CF, kind: "context", same envelope discipline as query traces): request hash, plan summary (per-leg candidate counts, dedup drops, cap skips, stranded budget), and the emitted item coordinates. GET /v1/trace/:id decodes it generically (the PR-052 generic-envelope reader already handles unknown kinds). This is the "why was this selected" record; "why was X NOT selected" beyond the counts is explicitly deferred (OQ-1).
  7. Permissions are the platform's, unchanged: tenant scope from API key or bearer exactly as /v1/query; items are read through the same executor/read paths, so anything the caller couldn't query, the caller cannot receive as context. MCP exposure (memoryContext tool) binds scope from the session token like every other tool and shapes output with the standard envelope.
  8. Cost: context calls do NOT touch the LLM cost ledger (nothing is spent); row/budget ceilings are the back-pressure. Metering is a dedicated namespace, separate from generate (OQ-2 resolution): context_calls, context_planner_ms, context_trace_writes, context_tokens_packed — context is cheaper than generation but not free (planner run + trace write per call), and PR-086b telemetry must be able to show whether per-tenant controls become necessary without disentangling shared counters.

4. Architecture

POST /v1/memory/context ─▶ auth (api-key | tenant bearer)      [existing middleware]
      └▶ resolve semantic leg (embed question if configured)   [existing]
      └▶ planner.plan(question, structured leg, window, B)     [PR-050, unchanged]
             legs: query ∪ partitions.search ∪ traversal ─ dedup ─ caps ─ budget
      └▶ map plan spans → items (+ pendingClarification refs)  [thin DTO layer]
      └▶ persist kind:"context" trace ─▶ response {items, openQuestions, stats, traceId}
MCP memoryContext ──▶ same handler, shaped envelope
/v1/generate ──▶ SAME planner entry point (parity invariant)

5. Data Models

No new storage schema. Response DTO (camelCase, versioned by the API not a byte): {items: [{text, source {sourceId, sourceVersionTx, start, end, name?}, score, validTime {start, end?}, pendingClarification? {clarificationId, state}}], openQuestions: [{clarificationId, state, question}], stats {legs {query, vector, traversal}, candidates, dedupDropped, capSkipped, budget {requested, applied, used, stranded}, vectorLegSkipped?}, traceId}. Context trace record (msgpack in traces CF): {v:1, kind:"context", request_hash (xxh3 of canonical request JSON), window, budget, item_coords: [(source_id, source_version_tx, start, end)], stats} — coordinates only, no span text duplication (slices re-resolve via the existing canonical_slice, the getTrace convention). Invariants: item ordering is the planner's deterministic order (score desc, ties by the planner's pinned total_cmp rule); stats cap/drop counts make truncation visible (no silent caps, per house rule).

6. API Contracts

POST /v1/memory/context — request {question?: string, find?: …, where?: … (query-language grammar, operator-object rule), window?: {validTime?: RFC3339|µs, txTime?: …}, budget?: {maxTokens?: u32, maxSpans?: u16}, include?: {openQuestions?: bool (true), scores?: bool (true), stats?: bool (true)}}; at least one of question/find required (400 CONTEXT_INVALID, pointered errors via the shared serde_path_to_error convention). Failure taxonomy: 400 CONTEXT_INVALID (shape/grammar/ceiling violations), 401/403 per platform auth, 200 with empty items + honest stats when memory has nothing (an empty plan is an answer, not an error). Budget over server ceiling → clamped + reported in stats.budget.applied, never an error. gRPC: deliberately NOT added in v1 (matches /v1/generate's REST-only posture; recorded follow-up when an SDK consumer demands it). SDKs: TS + Python gain memory.context(...) over REST (types mirror the DTO). MCP addendum (mcp-tools): memoryContext {question, validTime?, maxSpans?} → shaped items (standard truncation envelope, max_field_chars applies to span text).

7. UI/UX

Developer-facing surface, no end-user UI. This PR ships: telha memory context "<question>" [--valid-at] [--max-tokens] CLI (prints items with scores + coordinates — doubles as the planner debug tool the Memory Inspector idea wants), and the trace is inspectable via the existing GET /v1/trace/:id + MCP getTrace. The quickstart gains a "first memory call" section (telha memory context as step 4) — the developer-onboarding story leans on this endpoint.

8. Configuration

[context]: enabled (true — read-only, no egress, no new risk surface; disabling exists for operators who want the gateway dark), max_tokens_ceiling (8192), max_spans_ceiling (64), default_max_tokens (2000), trace (true). Planner knobs remain [planner]'s — this spec adds NO selection tuning of its own (that is §3.1). Deliberately NOT configurable: LLM independence, trace persistence when trace=true per-tenant… (v1: global flag only), parity with generate's planner path.

9. Alternatives Considered

  • Reuse /v1/generate with a dryRun/planOnly flag. Rejected: overloading a generation endpoint with a non-generating mode muddies auth/cost/failure semantics (callers must know which errors can still happen), and the response shape is items, not a draft with SSE modes. A distinct verb is the product (memory.context).
  • Return an LLM-written summary alongside items. Rejected for v1 (§3.4): it reintroduces the LLM dependency this endpoint exists to remove; agents compose their own prompts — that is the point.
  • Expose raw planner internals (leg provenance per item, rejection list). Rejected for v1: per-item leg attribution is cheap (kept in stats aggregate), but a full rejection list is O(candidates) response bloat; deferred as OQ-1 with the Inspector.
  • New context-specific retrieval tuned for agents (MMR diversity, recency boost). Rejected: would fork selection quality from generate's (violates §3.1); MMR is already a recorded planner deferral and lands in the planner for BOTH surfaces when it lands.
  • GraphQL/gRPC-first surface. Rejected: REST+MCP is where every existing consumer (SDKs, ai-assistant, connectors) already lives; generate set the precedent.

10. Implementation Approach

One small core PR (PR-089), spec first, independent of PR-087/088 (start anytime; cheapest of the phase): request DTO + validation → handler wiring planner + caveat lookup + trace persistence → REST route + OpenAPI derive + router-mirror test entry → CLI subcommand → MCP tool + tools.json regen → SDK methods (TS/Python) → §12 tests. The only planner-adjacent change is a pub(crate) seam to invoke the existing entry point with the caveat-annotation pass — no selection logic moves.

11. Migration Path

Purely additive: new route, new trace kind (generic reader already tolerates unknown kinds), new MCP tool, additive SDK methods. No storage, wire, or config migration. Forward compatibility: response gains fields additively; stats is extensible by design; request include flags default permissive so new flags never break old callers.

12. Success Metrics

  • context_matches_generate_plan: identical input (question, window, budget) yields the identical span-coordinate set and ordering as the plan /v1/generate executes — the §3.1 parity invariant, asserted at the plan object level, not by string comparison.
  • context_no_llm_required: endpoint returns correct items on a deployment with NO [llm] and NO [embedding] config (vector leg skipped + reported in stats); zero outbound connections during the call (matches the egress test harness from generation-orchestration).
  • context_budget_adherence: token budget respected within the planner spec's tolerance incl. its CJK property fixture; maxSpans exact; clamping reported, never erroring.
  • context_open_questions_parity: a pending clarification on a cited node appears in openQuestions exactly when /v1/query would attach the pendingClarification caveat to that node; conflicted state surfaces as conflicted.
  • context_bitemporal: validTime/txTime in the past returns as-known-then evidence (fixture mirrors the compare/eval "as known in June vs September" pattern); future vectors excluded (reuses the PR-040 as-of guarantee).
  • context_trace_roundtrip: every call persists a kind:"context" trace whose coordinates re-resolve to the returned item texts via canonical_slice; GET /v1/trace/:id decodes it.
  • context_tenant_isolation_and_shaping: cross-tenant coordinates never appear; MCP tool output obeys the standard truncation envelope with truncatedFields honesty.
  • context_empty_is_ok: a question with no relevant memory returns 200, empty items, honest stats — asserted against the eval harness rejection items (Telha's "I don't know" discipline extends to context).

13. Open Questions

  • OQ-1: "Why was X not selected" — full per-candidate rejection accounting? Resolved 2026-07-06 (J. Porter): aggregate stage counts only in v1 (candidates per leg → after dedup → after caps → packed → dropped-for-budget; the §5 stats shape); full candidate-level accounting is expensive and noisy — it arrives as a planner explain: true request flag with the Memory Inspector / dev mode, not in the default response.
  • OQ-2: Per-tenant disable / rate limits for context? Resolved 2026-07-06 (J. Porter): global ceilings in v1, no sophisticated tenant controls yet — but metering MUST be a separate namespace and quota surface from generate (§3.8: context_calls, context_planner_ms, context_trace_writes, context_tokens_packed) so PR-086b telemetry can show whether per-tenant controls are needed.
  • OQ-3: Coarse confidence bands (high/medium/low) on items? Resolved 2026-07-06 (J. Porter): no — uncalibrated bands invite reading "high = 90% true". v1 exposes raw scores plus explicit evidence/caveat FLAGS (the item type, pendingClarification {state} incl. conflicted, and validity coordinates); per-item score components (semantic/lexical/graph/temporal) ride the same future explain mode as OQ-1, not the default response.

14. Changelog

  • 2026-07-06 — v0.3: Approved (J. Porter). Ratified with the Phase-3d set once the memory-cards OQ-1 security countersign cleared the last gate; no design change from v0.2. Independent — startable in parallel with PR-087.
  • 2026-07-06 — v0.2: OQ review by J. Porter — all three resolved as drafted, one refinement incorporated. Metering pinned as a dedicated context_* telemetry/quota namespace separate from generate (§3.8) so PR-086b can judge per-tenant controls from data. Rejection accounting and per-item score components both routed to a future planner explain: true mode (default response stays compact); confidence bands confirmed out (raw scores + explicit caveat flags). Status → In Review.
  • 2026-07-06 — v0.1: initial draft (PR-089). One-planner-two-surfaces parity invariant; no-LLM/no-egress guarantee; span-grade items + clarification openQuestions; kind:"context" traces; no summary field in v1 (recorded deviation from the product plan's sketch); REST + CLI + MCP + SDK surfaces.