LearnNewsExamplesServices
Frontmatter
id12199
titleOrchestrator-driven low-latency on-disconnect summarization (restore ~100ms targeting)
stateClosed
labels
enhancementaiarchitectureperformance
assigneesneo-gpt
createdAtMay 30, 2026, 4:08 PM
updatedAtMay 31, 2026, 10:56 PM
githubUrlhttps://github.com/neomjs/neo/issues/12199
authorneo-opus-ada
commentsCount0
parentIssue12065
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 31, 2026, 10:56 PM

Orchestrator-driven low-latency on-disconnect summarization (restore ~100ms targeting)

Closed v13.0.0/archive-v13-0-0-chunk-14 enhancementaiarchitectureperformance
neo-opus-ada
neo-opus-ada commented on May 30, 2026, 4:08 PM

Context

Sub of #12065 (Orchestrator-as-SSOT for the REM pipeline). #12139 removed the per-MCP-server on-disconnect summarization path — SessionService.queueSummarizationJob + processPendingSummarizations, fired from Server.onSessionClosed — because it was a per-instance self-trigger that caused duplicate summarizations across multiple harness-spawned MCP instances (Claude Desktop / Codex / Antigravity each spawn N instances per server).

A post-merge exploration (operator-prompted: "check if we lose good ideas which are not on feature parity") confirmed the removal was correct for the drain (the expensive summarization work) but dropped a good idea: ~100ms low-latency targeting of the specific session that just ended.

The Problem

Post-#12139, a disconnected session is only summarized by the orchestrator's drift sweep (AiConfig.orchestrator.intervals.summarySweepMs, ~10 min) unless an unread sunset-handover message bumps priority. Coverage is fine — drift detection (findSessionsToSummarize, comparing raw-memory counts vs summary metadata) eventually catches every session — but latency regressed from ~100ms to up to ~10 min for the just-ended session.

The naive fix (restore the per-instance trigger) would re-introduce the duplicate-summarization problem #12139 removed. This ticket is the correct way to restore the capability.

The Architectural Reality

The duplicate problem came from multiple instances draining (running the summarization), not from writing a marker. The SummarizationJobs SQLite table (ai/graph/storage/SQLite.mjs:138-146) already has the right primitives:

  • A 'pending' status lane — now orphaned after #12139 (no writer, no reader; the deleted queueSummarizationJob was the only writer and processPendingSummarizations the only reader). The CHECK constraint still lists 'pending', and claimSummarizationJob's 'pending'→'in_progress' transition branch is now unreachable.
  • A lease (claimSummarizationJob'in_progress' + lease_token + 5-min TTL, ai/services/memory-core/SessionService.mjs) that already prevents double-processing and is used by the kept orchestrator path (summarizeSessions, called via ai/scripts/lifecycle/summarize-sessions.mjs).

So the 'pending' lane is currently dead weight, and the dedup lease is fully intact — the building blocks for a single-drainer low-latency path already exist.

The Fix

Restore low-latency disconnect summarization with the orchestrator as the sole drainer:

  1. Cheap idempotent marker (any instance): Server.onSessionClosed writes a 'pending' row to SummarizationJobs (the existing ON CONFLICT(session_id) DO UPDATE upsert is idempotent, so N instances writing the same session is harmless). It does not summarize inline.
  2. Single drainer (orchestrator only): the orchestrator summary task drains 'pending' rows on a short tick (in addition to its periodic drift sweep), claiming each via the existing lease before summarizing. The lease guarantees no double-processing even if the drift sweep and the pending-drain overlap.
  3. Net: the orphaned 'pending' lane becomes live again, owned end-to-end by the orchestrator drain. Latency for a just-ended session returns to seconds; zero per-instance summarization.

This also resolves the orphaned-status cleanup: either the 'pending' lane becomes live (this fix) or it + the dead claimSummarizationJob branch + the SQLite.mjs CHECK entry should be deleted as dead code. This ticket chooses "make it live."

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
Server.onSessionClosed ai/mcp/server/memory-core/Server.mjs write idempotent 'pending' marker; never summarize inline drift sweep still covers it method JSDoc post-#12139 source
orchestrator summary task ai/daemons/orchestrator/Orchestrator.mjs + scheduling/summary.mjs drain 'pending' rows (lease-claimed) on a short tick, distinct from the drift sweep drift sweep task JSDoc #12065
SummarizationJobs.'pending' ai/graph/storage/SQLite.mjs:138-146 re-activated lane (writer = onSessionClosed, reader = orchestrator drain) n/a schema
claimSummarizationJob lease ai/services/memory-core/SessionService.mjs unchanged — serializes the single drainer per session n/a existing

Decision Record impact

none — operates within the #12065 orchestrator-SSOT architecture; no ADR amendment. Reinforces #12139's "orchestrator is the sole drainer" principle rather than challenging it.

Acceptance Criteria

  • Server.onSessionClosed writes an idempotent 'pending' marker; it never summarizes inline.
  • The orchestrator summary task drains 'pending' rows (lease-claimed) on a short tick, distinct from the drift sweep.
  • A session that disconnects is summarized within seconds (not the full sweep interval), with exactly one summarizer.
  • Concurrency test: two overlapping drains (pending-drain + drift sweep) never double-summarize one session (lease holds). (Falsifying-input test, not just happy path.)
  • No dead 'pending' path remains — the lane is live end-to-end (writer + reader + lease transition all reachable).

Out of Scope

  • Hierarchical/chunking summarization quality#12073 (Sub 7) owns the how; this ticket is the when (trigger + latency).
  • Resurrecting any per-MCP-instance summarization drain — explicitly rejected (the regression #12139 removed).
  • Lowering the global drift-sweep interval as the latency fix — rejected: it trades CPU for latency globally and still doesn't target the specific just-ended session.

Avoided Traps

  • Restore the per-instance trigger — rejected: reintroduces the multi-instance duplicate-summarization problem #12139 removed.
  • Delete the 'pending' lane outright — viable alternative (it's currently orphaned), but discards the low-latency capability; this ticket revives it instead. If this ticket is declined, the fallback is to delete the orphaned 'pending' status + dead lease branch + CHECK entry as cleanup.

Related

  • Parent epic #12065 (orchestrator REM pipeline)
  • #12139 (removed the per-instance path; this restores the good idea correctly)
  • Sibling #12073 (Sub 7 — hierarchical-summarization strategy; the how to this ticket's when)

Origin Session ID: 94a91ebc-d325-4d32-a746-4ff8c26c0342

Handoff Retrieval Hints: query_raw_memories("on-disconnect summarization latency orchestrator pending lane lease"); commit anchor = #12139 merge (queueSummarizationJob removal).

tobiu referenced in commit aa09987 - "feat(orchestrator): restore pending summary markers (#12199) (#12273) on May 31, 2026, 10:56 PM
tobiu closed this issue on May 31, 2026, 10:56 PM