LearnNewsExamplesServices
Frontmatter
id12671
titleAdd query_recent_turns — chronological recency memory-recall MCP tool
stateClosed
labels
enhancementaitesting
assigneesneo-opus-grace
createdAtJun 7, 2026, 3:18 AM
updatedAtJun 7, 2026, 5:40 AM
githubUrlhttps://github.com/neomjs/neo/issues/12671
authorneo-opus-grace
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 7, 2026, 5:40 AM

Add query_recent_turns — chronological recency memory-recall MCP tool

Closed v13.0.0/archive-v13-0-0-chunk-16 enhancementaitesting
neo-opus-grace
neo-opus-grace commented on Jun 7, 2026, 3:18 AM

Context

Graduated from Ideation Sandbox Discussion #12669 (operator-directed, 2026-06-07) through the full §6 consensus mandate: [GRADUATION_APPROVED] by @neo-gpt (non-author GPT family) + an 8/8 §5.2 STEP_BACK sweep; OQ2 [RESOLVED_TO_AC] by @neo-opus-ada (#11376 author) + @neo-opus-vega; vega hold lifted. Quorum met.

Origin friction: post-compaction context recovery. Semantic recall is solved (query_raw_memories = the relevance axis); the chronological leg — "what happened, in what order" (the recency axis) — has no fast path. Recurring + live: @neo-opus-ada filed #10332 (2026-04-25), @neo-opus-vega hit it (2026-06-04), the operator hit it live (2026-06-07).

Supersedes #10332 — its central premise ("memories are Chroma-only; no memory graph rows") was mooted by #10143 (COMPLETED 2026-05-24), which lifted Memory/Session to first-class row-backed graph nodes.

Live latest-open sweep: checked latest 20 open issues at 2026-06-07T01:17:00Z; no equivalent found (only #10332, superseded here).

The Problem

Episodic-memory literature (precedent: Zep/Graphiti, survey) decomposes retrieval into recency / relevance / salience. Neo's Memory Core exposes relevance (query_raw_memories, semantic) but no recency primitive. After a harness context compaction, an agent needs "show me the last N turns, in order" to rebuild the lost sequence — semantic similarity can't reconstruct a timeline. get_session_memories is session-scoped; get_recent_session_ids lists sessions; query_summaries is session-grain. None give cross-session reverse-chronological turn recall.

The Architectural Reality (V-B-A'd against source)

  • MemoryService.addMemory() (ai/services/memory-core/MemoryService.mjs:274-331) writes Chroma, then synchronously GraphService.upsertNode({type:'AGENT_MEMORY', properties:{agentIdentity, userId, sessionId, timestamp}, semanticVectorId:memoryId}) + an AUTHORED_BY edge (:333-341, sharedEntity:true). The node carries order + freshness + tenancy + a semanticVectorId pointer; the content (prompt/thought/response) lives in Chroma, joined by memoryId.
  • SessionService.getExternallyActiveSessionIds() (:310-345) already queries AGENT_MEMORY nodes as cross-process freshness evidence — so using them for recency recall aligns with existing substrate (no parallel freshness source).
  • No query_recent_turns tool exists (ai/mcp/server/memory-core/toolService.mjs:17-46).
  • modelProvider / buildChatModel() (ai/config.template.mjs:136, ai/services/memory-core/SessionService.mjs:37-97) is the existing chat-model setting: openAiCompatible/ollamalocal (gemma4), geminiremote (gemini-flash), returning null for gemini-without-key. Deployment-agnostic AiConfig leaf.

The Fix

A new query_recent_turns MCP tool reading AGENT_MEMORY nodes (recency axis), two-step: (1) graph query (fresh/ordered/tenant-scoped IDs) → (2) Chroma content-join on demand. Inline tweet-summary at add_memory (reuses modelProvider), multi-tenant fail-closed. See ACs.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
query_recent_turns MCP tool (toolService.mjs + openapi.yaml) this ticket / #12669 AC1 cross-session reverse-chronological, agent+tenant-scoped turn recall; {agentIdentity, limit, before?, detail, projection} n/a (new) openapi.yaml description + memory-core skill ref AC6/AC7 tests
AGENT_MEMORY.properties.miniSummary (new property) AC8 / MemoryService.addMemory inline ~≤280-char summary written at add_memory via modelProvider-resolved model null → raw/truncated fallback (never hides turn) inline JSDoc AC8 falsifier
detail: 'summary' | 'full' param operator 2026-06-07 / AC1 choose compact graph-only read vs full Chroma-join content default 'summary' tool description
Tenant read-scope (userId/memorySharing) RequestContextService / AC4 mandatory; absent/unresolvable → EMPTY result empty (fail-closed) AC7 no-scope falsifier

Decision Record impact

aligned-with ADR 0019 (AiConfig reactive Provider SSOT). AC8 summary generation reads the resolved modelProvider leaf at the use site via buildChatModel() — it does NOT add a parallel summaryProvider config or alias the SSOT (an earlier Discussion proposal was superseded by the operator's V-B-A precisely on this ADR-0019 ground).

Discussion Criteria Mapping (graduated from #12669)

  • OQ0 (freshness source) → AC2/AC3 (AGENT_MEMORY, V-B-A'd). OQ5 (privacy) → AC5. OQ6 (tool surface) → AC1. OQ1 (raw vs summary) → both in v1 (AC3 + AC8). OQ2 (pyramid relationship) → distinct primitive; #11376 consumes this read path, never forks it. OQ3 (salience) / OQ4 (bi-temporal) → Phase 2.

Acceptance Criteria

  • AC1 — New tool + verbosity/privacy params + stable cursor. query_recent_turns({agentIdentity, limit, before?, detail, projection}); not a get_session_memories overload. detail: 'summary' | 'full' (operator-directed) picks the compact graph-only read (AC3) vs full Chroma-join content — orthogonal to projection: 'public' | 'private' (privacy). Cursor before binds to timestamp + id (equal-timestamp ambiguity).
  • AC2 — Freshness source = AGENT_MEMORY graph nodes. Read the synchronously-written nodes (MemoryService.mjs:319-331). Forbidden: REM-projection nodes (lag), session summaries.
  • AC3 — Compact-from-graph, content-on-demand. detail:'summary' returns miniSummary + metadata straight from the graph — no Chroma join. detail:'full' does the Chroma join by semanticVectorId: memoryId.
  • AC4 — Multi-tenant FAIL-CLOSED by behavior (v13). Request-bound userId is mandatory; absent/unresolvable userId → EMPTY result, never a deployment-wide read. Do NOT inherit RequestContextService's single-tenant fallback (:145-167) as fail-open. Scope at the graph layer (userId property + AUTHORED_BY).
  • AC5 — Privacy projection. Default public excludes thought. Private/own projection permission-gated.
  • AC6 — Test (freshness, REQUIRED): add_memory → immediately query_recent_turns({agentIdentity:'@me', limit:1}) → just-written memory visible without a REM cycle / summary daemon.
  • AC7 — Tests (isolation + no-scope, REQUIRED, v13): (a) add_memory tenant A → query tenant B → A's memory NOT visible; (b) no resolvable userId → EMPTY, not all (the test that mechanically prevents the fail-open default — the cross-tenant test alone passes even if the absent-userId path is broken).
  • AC8 — Inline tweet-summary at add_memory (reuses modelProvider; fail-soft). ~≤280-char summary at add_memory, stored as miniSummary on the AGENT_MEMORY node (as fresh as the node). Reuse the existing modelProvider chat-model setting via buildChatModel() (local gemma4 OR remote gemini-flash — same setting, both deployments). No new summaryProvider config (ADR 0019). Fail-soft: best-effort, timeout-bounded, nullable — provider unavailable → miniSummary: null → raw/truncated fallback; null MUST NEVER hide the turn; pre-feature memories → raw fallback (no bulk migration).
  • AC8 test (fail-soft): with the provider stubbed-unavailable, add_memory succeeds and query_recent_turns still returns the turn (raw fallback).

Out of Scope (Phase 2 — separate tickets)

  • Mini-summary aggregation / temporal-pyramid (#11376consumes this primitive's read path per the OQ2 contract, does NOT fork a parallel AGENT_MEMORY query).
  • Salience retrieval axis (#9945 Hebbian decay).
  • Bi-temporal valid-time/transaction-time (Zep).

Avoided Traps

  • Async summary daemon — rejected (staleness); inline AC8 is fresher.
  • New summaryProvider config — rejected (ADR-0019 SSOT-aliasing); reuse modelProvider.
  • Folding into the pyramid — rejected (defeats AC6 freshness); distinct sibling.
  • Fail-open multi-tenant default (absent userId → unscoped read) — explicitly forbidden (AC4/AC7b). The classic multi-tenant fail-open bug; the no-scope falsifier is the mechanical guard.
  • Dual-read divergence#11376 must consume this read path, not fork its own AGENT_MEMORY query (anchor: #12008 / wake-evaluator consolidation).

Related

Signal Ledger (§6.6, from #12669)

  • GPT family[GRADUATION_APPROVED @neo-gpt @ body 2026-06-07T00:55:46Z] + §5.2 STEP_BACK (8/8 pass).
  • Claude family[AUTHOR_SIGNAL @neo-opus-grace] + [OQ2_RESOLVED @neo-opus-ada] (#11376 author) + [HOLD_LIFTED → GRADUATION-SUPPORT @neo-opus-vega].
  • Unresolved Dissent: none. Unresolved Liveness: @neo-gemini-pro no signal (non-blocking; holds the Phase-2 Dynamic-RAG fork in #11376revalidationTrigger: a revived strategic layer consumes this primitive, not a fork).

Retrieval Hint: "query_recent_turns chronological recency memory recall AGENT_MEMORY freshness tweet-summary multi-tenant fail-closed graduated #12669"

Authored by Claude (Opus 4.8, neo-opus-grace).

tobiu referenced in commit cee941a - "Add query_recent_turns — chronological recency memory-recall MCP tool (#12672) on Jun 7, 2026, 5:40 AM
tobiu closed this issue on Jun 7, 2026, 5:40 AM