From 1b43332c742f9fb5d30aaf2b0f2c6be637c2e66d Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 25 Oct 2026 09:40:41 -0500 Subject: [PATCH] fix(server): release settled voice recovery intent during drain Co-Authored-By: Paperclip --- doc/plans/2026-10-25-voice-drain-repair.md | 34 +++++++++++++++++++ .../src/__tests__/voice-recovery-work.test.ts | 11 +++++- .../src/services/voice/voice-recovery-work.ts | 4 ++- 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 doc/plans/2026-10-25-voice-drain-repair.md diff --git a/doc/plans/2026-10-25-voice-drain-repair.md b/doc/plans/2026-10-25-voice-drain-repair.md new file mode 100644 index 00000000000..8b327b1c572 --- /dev/null +++ b/doc/plans/2026-10-25-voice-drain-repair.md @@ -0,0 +1,34 @@ +# Voice recovery drain repair + +Owner: slice 21, PR #17576. Reviewed baseline: 1bf70185d2. + +Greptile finding 4370544173 is valid. The worker retained dirty scopes while +paused, and reported those in-memory hints as pending work. After the producer +committed, the coordinator therefore retained its intent activity hold. No SQL +was required to reproduce the blocked drain; the earlier quiet assertion checked +SQL only and missed the nonzero idle counter. + +The worker now reports pending dirty scopes only while dispatch is permitted. +It still retains every hint and next deadline. The coordinator independently +checks databaseWorkPending, so an open/uncertain transaction retains its activity +hold until transaction reconciliation establishes settlement. Paused timers then +use the existing passive-maintenance path without changing the idle generation. +Resume processes the retained first session without another producer signal. +No provider behavior or external sleep eligibility rule changes. + +The strengthened real-PostgreSQL test proves: an open write during drain retains +an activity hold; commit releases it; a further minute causes neither SQL nor +an idle-snapshot change; resume processes the one retained session exactly once. + +- /tmp/sleep21-drain-before.log: original worker fails, active is 1 instead of 0. +- /tmp/sleep21-drain-tests.log: all 11 voice recovery lifecycle tests pass. +- /tmp/sleep21-drain-typecheck.log: production server typecheck passes. +- Dedicated migrated database: paperclip_sleep21_drain_20261025_test. + +Propagate this predicate into slice 22's extracted scoped-deadline worker and +all descendants, then rerun its existing consumers. The earlier tool-health, +Discord command, and receiver workers have the same predicate pattern; audit +and repair those at their own parent PRs as a separate propagation pass. Do not +report all stack drain behavior fixed from the voice test alone. The company +inbound cleanup/deletion policy, older E2E failure, current chat milestone audit, +and full original inventory remain outstanding. diff --git a/server/src/__tests__/voice-recovery-work.test.ts b/server/src/__tests__/voice-recovery-work.test.ts index 946209e55cb..cde5cacfecf 100644 --- a/server/src/__tests__/voice-recovery-work.test.ts +++ b/server/src/__tests__/voice-recovery-work.test.ts @@ -90,7 +90,16 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu await settled(); expect(inspect).toHaveBeenCalledOnce(); await quiet(); }); it("preserves the first session during drain without passive database reads", async () => { - await start(); await settled(); enabled = false; await session(); await quiet(60_000); expect(inspect).not.toHaveBeenCalled(); + await start(); await settled(); enabled = false; + await db.transaction(async tx => { + await session({}, tx as unknown as typeof db); + await vi.advanceTimersByTimeAsync(60_000); + expect(idleWorkSnapshot().active).toBeGreaterThan(0); + expect(inspect).not.toHaveBeenCalled(); + }); + await vi.waitFor(async () => { await vi.advanceTimersByTimeAsync(1000); expect(idleWorkSnapshot().active).toBe(0); }); + const drained = idleWorkSnapshot(); + await quiet(60_000); expect(idleWorkSnapshot()).toEqual(drained); expect(inspect).not.toHaveBeenCalled(); enabled = true; await settled(); expect(inspect).toHaveBeenCalledOnce(); await quiet(); }); it("retries unavailable authority without ending a valid provider call", async () => { diff --git a/server/src/services/voice/voice-recovery-work.ts b/server/src/services/voice/voice-recovery-work.ts index 85da30c2d31..18f6f77b89a 100644 --- a/server/src/services/voice/voice-recovery-work.ts +++ b/server/src/services/voice/voice-recovery-work.ts @@ -78,7 +78,9 @@ export function registerVoiceRecoveryWork( } if (errors.length) throw new AggregateError(errors, "Voice session recovery could not settle"); }, - hasPending: async () => dirty.size > 0, + // Retain scopes/deadlines for resume without retaining a settled write's + // idle hold during drain. The coordinator still fences unresolved XIDs. + hasPending: async () => canRun() && dirty.size > 0, nextRunAt: next, }); return worker;