From 5fd491716a26a8d12e5376295eec265166ac32ed Mon Sep 17 00:00:00 2001 From: Paul Robertson Date: Sat, 15 Aug 2026 04:25:54 +0000 Subject: [PATCH] fix(core): bound startup recovery wait in shutdown to 15s core.shutdown() awaited startupRecoveryPromise with no timeout. With a large dirty episode and a slow/flaky LLM, the recovery reflect chain can take minutes, holding shutdown hostage until the systemd kill timer (observed 15 Aug 2026: SIGTERM 08:00:23 -> SIGKILL 08:10:23, 10-minute stop-sigterm wedge). Recovery is resumable: dirty episodes carry rewardDirty.failedAttempts and the periodic rescore re-runs them, so nothing is lost by proceeding after a short grace. The 15s bound still covers the fast init->shutdown SQLite race (issue #1808) that the wait was introduced for. --- apps/memos-local-plugin/core/pipeline/memory-core.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/memos-local-plugin/core/pipeline/memory-core.ts b/apps/memos-local-plugin/core/pipeline/memory-core.ts index 01402a86b..fbef633d4 100644 --- a/apps/memos-local-plugin/core/pipeline/memory-core.ts +++ b/apps/memos-local-plugin/core/pipeline/memory-core.ts @@ -1933,7 +1933,14 @@ export function createMemoryCore( // gateway reload would close SQLite while reflect / reward is // mid-flush, producing `SQLITE_MISUSE` noise on the way down. try { - await startupRecoveryPromise; + // Bound the wait: a slow / flaky LLM during startup recovery of a + // large dirty episode must not hold shutdown hostage until the + // systemd kill timer (15 Aug 2026 stop-sigterm wedge: SIGTERM at + // 08:00:23, SIGKILL at 08:10:23). Recovery is resumable — dirty + // episodes carry rewardDirty.failedAttempts and the periodic + // rescore re-runs them — so nothing is lost by proceeding after a + // short grace. Fast init→shutdown races still get their grace. + await withTimeout(startupRecoveryPromise, 15_000, "startup_recovery_shutdown_timeout"); } catch { /* already logged inside the recovery promise */ }