#16677 records a live Memory Core failure family where the process remains alive while MCP calls time out. #16685 / PR #16686 added the first required discriminator: a redacted telemetry row is persisted before tool dispatch and remains visible as unfinishedCalls until the call completes.
The deployed recorder exposed a second, measured gap. At 2026-08-08T17:03Z, get_memory_core_tool_metrics({sinceMs: 21600000, limit: 30}) reported zero unfinished calls, while its completed aggregates contained:
add_message max duration: 474,347 ms;
healthcheck max duration: 283,564 ms;
list_messages max duration: 283,526 ms;
query_raw_memories max duration: 71,886 ms.
The SDK caller deadline is 60 seconds. These rows prove that some operations crossed the server dispatch boundary and returned from server code minutes after the client had timed out. A same-day peer receipt also captured an add_message timeout that did not land at all. The two signatures cannot be correlated after recovery because completed rows collapse into per-tool aggregates.
Problem
The unfinished-call observer is useful only while a row remains unfinished. When a multi-minute call eventually completes, its exact start, completion, outcome, and opaque call ID disappear from the public diagnostic projection. An operator arriving after self-recovery sees only a maximum duration mixed into an aggregate and cannot reconstruct which call occupied the incident window.
The underlying SQLite row already carries the required redacted facts. The gap is projection, not storage.
Architectural Reality
ai/services/memory-core/MemoryCoreRecorderService.mjs persists one row per call with opaque ID, tool, start timestamp, completion timestamp, duration, success, and failure stage.
get_memory_core_tool_metrics currently returns completed per-tool aggregates plus bounded oldest-first unfinished rows.
ai/mcp/server/memory-core/openapi.yaml owns the public response and input contract.
Raw arguments, results, agent IDs, user IDs, and session IDs are intentionally excluded from public unfinished telemetry and remain excluded here.
Recorder writes are best-effort with a 50 ms SQLite busy timeout, so absence of a row is not proof that a request never reached the server.
Intended Solution
Extend the existing metrics operation; do not add another MCP tool.
MemoryCoreRecorderService.getMemoryCoreToolMetrics({sinceMs, limit, slowAfterMs = 60000}) will return:
the existing completed aggregates;
the existing unfinished rows;
slowAfterMs, the effective completed-call threshold; and
recentSlowCalls, newest-completed first and bounded by the existing effective limit.
The default 60,000 ms threshold matches the ordinary MCP SDK request deadline documented by the canonical plane client. Callers may lower or raise it for a bounded incident window; invalid/non-positive values fall back to the default. Disabled or unavailable telemetry returns an empty slow-call list.
Correlate a prior unfinished call with its eventual slow completion after self-recovery
A slow server completion does not prove the client received the response
Parent comment + post-deploy receipt
Live exact-row receipt
Acceptance Criteria
Completed calls at or above slowAfterMs appear in recentSlowCalls; faster and out-of-window rows do not.
Rows are newest-completed first with deterministic tie ordering and are bounded by the effective existing limit.
Each row exposes exactly callId, tool, startedAt, completedAt, durationMs, success, and failureStage.
Raw arguments/results and agent, user, and session identifiers cannot appear in the projection.
Invalid/non-positive slowAfterMs falls back to 60,000 ms; disabled/unavailable states disclose the effective threshold and return recentSlowCalls: [].
Existing aggregate and unfinished-call contracts remain unchanged.
OpenAPI input/output schemas and service parity stay green.
Focused Neo unit coverage fails against the pre-change projection and passes after the change.
A post-deploy read captures the exact redacted rows behind at least one previously aggregate-only multi-minute maximum, or records NOT_YET_MEASURED if the deployment starts a fresh telemetry epoch.
Out of Scope
Claiming the operation behind #16677 is the root cause merely because it was slow.
Event-loop, heap/GC, pulse, or container-health instrumentation.
Automatic restart, restore, replay, retry, or client deduplication.
Persisting raw request/response payloads or caller identity in the public projection.
Live premise check: deployed get_memory_core_tool_metrics returned 2,672 completed calls, zero unfinished calls, and 283–474 second per-tool maxima on the current process.
Source census: the required per-call fields already exist in mc_tool_call_log; only the public projection is missing.
Duplicate sweep: latest open queue, all-state GitHub title/body search, Knowledge Base ticket search, and current A2A claims checked; no equivalent ticket or competing claim found. The closest KB hit (#12743) is an unrelated historical cost ledger.
Memory Core archaeology: two targeted searches returned explicit Chroma/embedding degradation, not genuine no-match; this gap is disclosed rather than converted into a clean precedent claim.
Context
#16677 records a live Memory Core failure family where the process remains alive while MCP calls time out. #16685 / PR #16686 added the first required discriminator: a redacted telemetry row is persisted before tool dispatch and remains visible as
unfinishedCallsuntil the call completes.The deployed recorder exposed a second, measured gap. At 2026-08-08T17:03Z,
get_memory_core_tool_metrics({sinceMs: 21600000, limit: 30})reported zero unfinished calls, while its completed aggregates contained:add_messagemax duration: 474,347 ms;healthcheckmax duration: 283,564 ms;list_messagesmax duration: 283,526 ms;query_raw_memoriesmax duration: 71,886 ms.The SDK caller deadline is 60 seconds. These rows prove that some operations crossed the server dispatch boundary and returned from server code minutes after the client had timed out. A same-day peer receipt also captured an
add_messagetimeout that did not land at all. The two signatures cannot be correlated after recovery because completed rows collapse into per-tool aggregates.Problem
The unfinished-call observer is useful only while a row remains unfinished. When a multi-minute call eventually completes, its exact start, completion, outcome, and opaque call ID disappear from the public diagnostic projection. An operator arriving after self-recovery sees only a maximum duration mixed into an aggregate and cannot reconstruct which call occupied the incident window.
The underlying SQLite row already carries the required redacted facts. The gap is projection, not storage.
Architectural Reality
ai/services/memory-core/MemoryCoreRecorderService.mjspersists one row per call with opaque ID, tool, start timestamp, completion timestamp, duration, success, and failure stage.get_memory_core_tool_metricscurrently returns completed per-tool aggregates plus bounded oldest-first unfinished rows.ai/mcp/server/memory-core/openapi.yamlowns the public response and input contract.Intended Solution
Extend the existing metrics operation; do not add another MCP tool.
MemoryCoreRecorderService.getMemoryCoreToolMetrics({sinceMs, limit, slowAfterMs = 60000})will return:slowAfterMs, the effective completed-call threshold; andrecentSlowCalls, newest-completed first and bounded by the existing effectivelimit.Each slow row is a redacted projection:
{callId, tool, startedAt, completedAt, durationMs, success, failureStage}The default 60,000 ms threshold matches the ordinary MCP SDK request deadline documented by the canonical plane client. Callers may lower or raise it for a bounded incident window; invalid/non-positive values fall back to the default. Disabled or unavailable telemetry returns an empty slow-call list.
Contract Ledger
MemoryCoreRecorderService.getMemoryCoreToolMetrics({sinceMs, limit, slowAfterMs = 60000})slowAfterMsand boundedrecentSlowCallswithout changing existing fieldsrecentSlowCalls[]mc_tool_call_logrow lifecyclesinceMsare excludedunknown, never “request did not dispatch”get_memory_core_tool_metricsobserverAcceptance Criteria
slowAfterMsappear inrecentSlowCalls; faster and out-of-window rows do not.limit.callId,tool,startedAt,completedAt,durationMs,success, andfailureStage.slowAfterMsfalls back to 60,000 ms; disabled/unavailable states disclose the effective threshold and returnrecentSlowCalls: [].NOT_YET_MEASUREDif the deployment starts a fresh telemetry epoch.Out of Scope
Avoided Traps
Decision Record Impact
None. This extends the existing redacted observer without adding a diagnosis/action class or changing ADR 0025/0026 recovery authority.
Related
Creation-Gate Record
get_memory_core_tool_metricsreturned 2,672 completed calls, zero unfinished calls, and 283–474 second per-tool maxima on the current process.mc_tool_call_log; only the public projection is missing.Origin Session ID
019fe0b3-53bc-7ef2-8665-41a0ef3f7b62Retrieval Hint
Memory Core recent slow completed calls exact timestamps mc_tool_call_log #16677 #16685