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:
- 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.
- 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.
- 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
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).
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 fromServer.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
SummarizationJobsSQLite table (ai/graph/storage/SQLite.mjs:138-146) already has the right primitives:'pending'status lane — now orphaned after #12139 (no writer, no reader; the deletedqueueSummarizationJobwas the only writer andprocessPendingSummarizationsthe only reader). TheCHECKconstraint still lists'pending', andclaimSummarizationJob's'pending'→'in_progress'transition branch is now unreachable.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 viaai/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:
Server.onSessionClosedwrites a'pending'row toSummarizationJobs(the existingON CONFLICT(session_id) DO UPDATEupsert is idempotent, so N instances writing the same session is harmless). It does not summarize inline.summarytask 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.'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 deadclaimSummarizationJobbranch + theSQLite.mjsCHECK entry should be deleted as dead code. This ticket chooses "make it live."Contract Ledger
Server.onSessionClosedai/mcp/server/memory-core/Server.mjs'pending'marker; never summarize inlinesummarytaskai/daemons/orchestrator/Orchestrator.mjs+scheduling/summary.mjs'pending'rows (lease-claimed) on a short tick, distinct from the drift sweepSummarizationJobs.'pending'ai/graph/storage/SQLite.mjs:138-146claimSummarizationJobleaseai/services/memory-core/SessionService.mjsDecision 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.onSessionClosedwrites an idempotent'pending'marker; it never summarizes inline.summarytask drains'pending'rows (lease-claimed) on a short tick, distinct from the drift sweep.'pending'path remains — the lane is live end-to-end (writer + reader + lease transition all reachable).Out of Scope
Avoided Traps
'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
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).