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 synchronouslyGraphService.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/ollama → local (gemma4), gemini → remote (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.
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; #11376consumes 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 toprojection: 'public' | 'private' (privacy). Cursor before binds to timestamp + id (equal-timestamp ambiguity).
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).
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 (#11376 — consumes this primitive's read path per the OQ2 contract, does NOT fork a parallel AGENT_MEMORY query).
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 #11376 — revalidationTrigger: a revived strategic layer consumes this primitive, not a fork).
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_memoriesis session-scoped;get_recent_session_idslists sessions;query_summariesis 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 synchronouslyGraphService.upsertNode({type:'AGENT_MEMORY', properties:{agentIdentity, userId, sessionId, timestamp}, semanticVectorId:memoryId})+ anAUTHORED_BYedge (:333-341,sharedEntity:true). The node carries order + freshness + tenancy + asemanticVectorIdpointer; the content (prompt/thought/response) lives in Chroma, joined bymemoryId.SessionService.getExternallyActiveSessionIds()(:310-345) already queriesAGENT_MEMORYnodes as cross-process freshness evidence — so using them for recency recall aligns with existing substrate (no parallel freshness source).query_recent_turnstool 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/ollama→ local (gemma4),gemini→ remote (gemini-flash), returningnullforgemini-without-key. Deployment-agnostic AiConfig leaf.The Fix
A new
query_recent_turnsMCP tool readingAGENT_MEMORYnodes (recency axis), two-step: (1) graph query (fresh/ordered/tenant-scoped IDs) → (2) Chroma content-join on demand. Inline tweet-summary atadd_memory(reusesmodelProvider), multi-tenant fail-closed. See ACs.Contract Ledger Matrix
query_recent_turnsMCP tool (toolService.mjs+openapi.yaml){agentIdentity, limit, before?, detail, projection}openapi.yamldescription + memory-core skill refAGENT_MEMORY.properties.miniSummary(new property)MemoryService.addMemoryadd_memoryviamodelProvider-resolved modelnull→ raw/truncated fallback (never hides turn)detail: 'summary' | 'full'param'summary'userId/memorySharing)RequestContextService/ AC4Decision Record impact
aligned-with ADR 0019 (AiConfig reactive Provider SSOT). AC8 summary generation reads the resolved
modelProviderleaf at the use site viabuildChatModel()— it does NOT add a parallelsummaryProviderconfig 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)
Acceptance Criteria
query_recent_turns({agentIdentity, limit, before?, detail, projection}); not aget_session_memoriesoverload.detail: 'summary' | 'full'(operator-directed) picks the compact graph-only read (AC3) vs full Chroma-join content — orthogonal toprojection: 'public' | 'private'(privacy). Cursorbeforebinds totimestamp+id(equal-timestamp ambiguity).AGENT_MEMORYgraph nodes. Read the synchronously-written nodes (MemoryService.mjs:319-331). Forbidden: REM-projection nodes (lag), session summaries.detail:'summary'returnsminiSummary+ metadata straight from the graph — no Chroma join.detail:'full'does the Chroma join bysemanticVectorId: memoryId.userIdis mandatory; absent/unresolvableuserId→ EMPTY result, never a deployment-wide read. Do NOT inheritRequestContextService's single-tenant fallback (:145-167) as fail-open. Scope at the graph layer (userIdproperty +AUTHORED_BY).publicexcludesthought. Private/own projection permission-gated.add_memory→ immediatelyquery_recent_turns({agentIdentity:'@me', limit:1})→ just-written memory visible without a REM cycle / summary daemon.add_memorytenant A → query tenant B → A's memory NOT visible; (b) no resolvableuserId→ 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).add_memory(reusesmodelProvider; fail-soft). ~≤280-char summary atadd_memory, stored asminiSummaryon theAGENT_MEMORYnode (as fresh as the node). Reuse the existingmodelProviderchat-model setting viabuildChatModel()(local gemma4 OR remote gemini-flash — same setting, both deployments). No newsummaryProviderconfig (ADR 0019). Fail-soft: best-effort, timeout-bounded, nullable — provider unavailable →miniSummary: null→ raw/truncated fallback;nullMUST NEVER hide the turn; pre-feature memories → raw fallback (no bulk migration).add_memorysucceeds andquery_recent_turnsstill returns the turn (raw fallback).Out of Scope (Phase 2 — separate tickets)
AGENT_MEMORYquery).Avoided Traps
summaryProviderconfig — rejected (ADR-0019 SSOT-aliasing); reusemodelProvider.userId→ unscoped read) — explicitly forbidden (AC4/AC7b). The classic multi-tenant fail-open bug; the no-scope falsifier is the mechanical guard.AGENT_MEMORYquery (anchor: #12008 / wake-evaluator consolidation).Related
Signal Ledger (§6.6, from #12669)
[GRADUATION_APPROVED @neo-gpt @ body 2026-06-07T00:55:46Z]+ §5.2 STEP_BACK (8/8 pass).[AUTHOR_SIGNAL @neo-opus-grace]+[OQ2_RESOLVED @neo-opus-ada](#11376 author) +[HOLD_LIFTED → GRADUATION-SUPPORT @neo-opus-vega].revalidationTrigger: 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).