Resolves #12833
SessionService.summarizeSession's synthesis call had no timeout — a slow in-band local synthesis ground to the provider socket cap (the ~30-min stall @neo-opus-ada surfaced root-causing #12830), stranding SummarizationJobs leases past their 5-min TTL (the 6 stuck in_progress-past-expiry jobs) and starving the REM cycle that defers behind the session-summary HeavyMaintenanceLease. This bounds the synthesis and degrades gracefully instead.
Authored by Claude Opus 4.8 (@neo-opus-vega).
Evidence: L1 (unit: timeout→degraded-fallback + within/over-band paths, mocked provider; withTimeout re-export; config-leaf migration confirmed) → L3 required for the live "synthesis aborts at the budget + no lease stuck past expiry" behavior. Residual: live timeout/lease behavior [#12833 Post-Merge].
What shipped
SessionService.summarizeSession — the synthesis call is now bounded: generateContent(prompt, {timeoutMs, operationLabel}) raced with a withTimeout backstop. The degraded-fallback trigger (previously size-precheck-skip only) now also fires on the timeout friction symptom, so a too-slow raw synthesis retries the compact (miniSummary/truncatedRaw) input instead of returning null.
sessionSummaryTimeoutMs — new env-overridable leaf (180000) in the memory-core config. Calibrated below the 300000ms SummarizationJobs lease TTL (claimSummarizationJob default) so the raw synthesis aborts before its own lease expires, leaving ~120s headroom for the degraded retry to finish within the lease. A synthesis approaching the lease TTL is already heading for the stuck-lease bug, so degrading it via the fast compact retry is the correct outcome.
withTimeout extracted from MemoryService to ai/services/memory-core/helpers/withTimeout.mjs (shared without a MemoryService ⇄ SessionService import cycle; re-exported from MemoryService so its existing spec is untouched).
Deltas from ticket
- The ticket's
consumerFrictionHelper.categorizeInvocationError → PROVIDER_TIMEOUT_CODE structural adoption is deferred to a small follow-up. The degrade-on-timeout path already works via the existing /timed out/i regex (the provider's PROVIDER_TIMEOUT message matches it), so the structural check is a non-blocking robustness upgrade. Deferred here to keep this PR scoped to the SessionService stall fix and avoid churning unrelated grandfathered ticket-refs in that file's comments (a check-ticket-archaeology pre-commit friction). I'll carry it as a follow-up — it's also the sibling of the ask-path adoption in PR #12832, per the split with @neo-opus-grace.
- Timeout default anchored to the lease TTL (180s < 300s) rather than a fresh model measurement (deferred — see Post-Merge Validation).
Test Evidence
npm run test-unit -- SessionSummaryDegradedFallback consumerFrictionHelper WithTimeout → 29 passed, incl. the new in-band raw that TIMES OUT → degraded compact summary regression + the existing oversize/within-band paths + the MemoryService.WithTimeout re-export spec.
- The ESM import-race flake (#12693) surfaced transient failures in the untouched
SessionService.ResumeValidation spec under concurrent workers; confirmed unrelated via an isolated 9/9 re-run.
SessionSummarization live-gemma4 tests are CI-skipped (NEO_TEST_SKIP_CI, bucket A #10903) + gated on a reachable local model; the 300s timeouts in local runs are the busy local endpoint, not this change (the 300s per-test cap is hit regardless of the 180s synthesis budget).
- Config-leaf resolution verified:
initServerConfigs --migrate-config propagates sessionSummaryTimeoutMs: leaf(180000) into the gitignored overlay; CI generates a fresh overlay from the template.
Config-template change (per mcp-config-template-change-guide)
- Added key:
sessionSummaryTimeoutMs (180000, env NEO_MEMORY_SESSION_SUMMARY_TIMEOUT_MS) in ai/mcp/server/memory-core/config.template.mjs.
- Local
config.mjs follow-up: each clone's gitignored config.mjs lacks the new leaf until regenerated — run npm run prepare -- --migrate-config (or node ./ai/scripts/setup/initServerConfigs.mjs --migrate-config). Until then aiConfig.sessionSummaryTimeoutMs resolves undefined; CI generates a fresh overlay so this is a clone-local step.
- Harness restart: recommended for the memory-core server to load the new budget.
- No gitignored
config.mjs committed.
Post-Merge Validation
Related
- #12065 — [Epic] Orchestrator-as-SSOT for the REM (Sandman) Pipeline (parent)
- #12830 — corrupted-memory root-cause (@neo-opus-ada; handed this lane the L576 finding)
- #12831 / PR #12832 — sibling KB-ask degraded-references (the
PROVIDER_TIMEOUT_CODE split)
- #12805 / #12722 — miniSummary timeout right-size / abort-hung (the mirrored pattern)
- #12814 / #12818 —
PROVIDER_TIMEOUT_CODE provider-timeout contract
Resolves #12833
SessionService.summarizeSession's synthesis call had no timeout — a slow in-band local synthesis ground to the provider socket cap (the ~30-min stall @neo-opus-ada surfaced root-causing #12830), strandingSummarizationJobsleases past their 5-min TTL (the 6 stuckin_progress-past-expiry jobs) and starving the REM cycle that defers behind the session-summaryHeavyMaintenanceLease. This bounds the synthesis and degrades gracefully instead.Authored by Claude Opus 4.8 (@neo-opus-vega).
Evidence: L1 (unit: timeout→degraded-fallback + within/over-band paths, mocked provider;
withTimeoutre-export; config-leaf migration confirmed) → L3 required for the live "synthesis aborts at the budget + no lease stuck past expiry" behavior. Residual: live timeout/lease behavior [#12833 Post-Merge].What shipped
SessionService.summarizeSession— the synthesis call is now bounded:generateContent(prompt, {timeoutMs, operationLabel})raced with awithTimeoutbackstop. The degraded-fallback trigger (previouslysize-precheck-skiponly) now also fires on thetimeoutfriction symptom, so a too-slow raw synthesis retries the compact (miniSummary/truncatedRaw) input instead of returningnull.sessionSummaryTimeoutMs— new env-overridable leaf (180000) in the memory-core config. Calibrated below the 300000msSummarizationJobslease TTL (claimSummarizationJobdefault) so the raw synthesis aborts before its own lease expires, leaving ~120s headroom for the degraded retry to finish within the lease. A synthesis approaching the lease TTL is already heading for the stuck-lease bug, so degrading it via the fast compact retry is the correct outcome.withTimeoutextracted fromMemoryServicetoai/services/memory-core/helpers/withTimeout.mjs(shared without aMemoryService ⇄ SessionServiceimport cycle; re-exported fromMemoryServiceso its existing spec is untouched).Deltas from ticket
consumerFrictionHelper.categorizeInvocationError→PROVIDER_TIMEOUT_CODEstructural adoption is deferred to a small follow-up. The degrade-on-timeout path already works via the existing/timed out/iregex (the provider'sPROVIDER_TIMEOUTmessage matches it), so the structural check is a non-blocking robustness upgrade. Deferred here to keep this PR scoped to the SessionService stall fix and avoid churning unrelated grandfathered ticket-refs in that file's comments (acheck-ticket-archaeologypre-commit friction). I'll carry it as a follow-up — it's also the sibling of the ask-path adoption in PR #12832, per the split with @neo-opus-grace.Test Evidence
npm run test-unit -- SessionSummaryDegradedFallback consumerFrictionHelper WithTimeout→ 29 passed, incl. the newin-band raw that TIMES OUT → degraded compact summaryregression + the existing oversize/within-band paths + theMemoryService.WithTimeoutre-export spec.SessionService.ResumeValidationspec under concurrent workers; confirmed unrelated via an isolated 9/9 re-run.SessionSummarizationlive-gemma4 tests are CI-skipped (NEO_TEST_SKIP_CI, bucket A #10903) + gated on a reachable local model; the 300s timeouts in local runs are the busy local endpoint, not this change (the 300s per-test cap is hit regardless of the 180s synthesis budget).initServerConfigs --migrate-configpropagatessessionSummaryTimeoutMs: leaf(180000)into the gitignored overlay; CI generates a fresh overlay from the template.Config-template change (per mcp-config-template-change-guide)
sessionSummaryTimeoutMs(180000, envNEO_MEMORY_SESSION_SUMMARY_TIMEOUT_MS) inai/mcp/server/memory-core/config.template.mjs.config.mjsfollow-up: each clone's gitignoredconfig.mjslacks the new leaf until regenerated — runnpm run prepare -- --migrate-config(ornode ./ai/scripts/setup/initServerConfigs.mjs --migrate-config). Until thenaiConfig.sessionSummaryTimeoutMsresolvesundefined; CI generates a fresh overlay so this is a clone-local step.config.mjscommitted.Post-Merge Validation
config.mjson active clones (--migrate-config) + restart the memory-core server sosessionSummaryTimeoutMsresolves.SummarizationJobslease stuckin_progresspast expiry.sessionSummaryTimeoutMsagainst a worst-case session-synthesis measurement (must stay below the 300s lease TTL).Related
PROVIDER_TIMEOUT_CODEsplit)PROVIDER_TIMEOUT_CODEprovider-timeout contract