From 954a0faa758711a5c06e5994a7d1d865c2427e6a Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 25 Oct 2026 10:01:57 -0500 Subject: [PATCH] fix(server): release settled Discord work during drain Co-Authored-By: Paperclip --- ...2026-10-25-configuration-worker-drain-repair.md | 14 ++++++++++++++ .../services/chat-channels/discord/command-work.ts | 4 +++- ...chat-discord-command-registration-store.test.ts | 4 +++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/doc/plans/2026-10-25-configuration-worker-drain-repair.md b/doc/plans/2026-10-25-configuration-worker-drain-repair.md index 99ebdb41834..9b2d8c64483 100644 --- a/doc/plans/2026-10-25-configuration-worker-drain-repair.md +++ b/doc/plans/2026-10-25-configuration-worker-drain-repair.md @@ -29,3 +29,17 @@ Discord command registration and receiver discovery require the same causal check at their own parents, then propagation through every dependent stack PR. The full original recurring-work inventory, milestone producer audit, older E2E failure and inbound cleanup/deletion policy finding remain outstanding. + +## Discord command registration — slice 19 / #17557 + +The strengthened outer-rollback/drain case reproduces active=1 after commit in +/tmp/sleep19-drain-before.log. The repair preserves the dirty scope and saved +deadlines, but reports pending dirty work only while dispatch is allowed. The +shared coordinator still holds unresolved transactions independently. + +All 67 registration/store cases pass in /tmp/sleep19-drain-tests.log, including +90 seconds of no SQL/provider calls and an unchanged idle snapshot during drain, +followed by registration on resume without another producer. Production server +typecheck passes in /tmp/sleep19-drain-typecheck.log. These tests use the migrated +paperclip_sleep19_drain_20261025_test database. Remote CI/review must run on the +new head; this is not evidence of provider suspension. diff --git a/server/src/services/chat-channels/discord/command-work.ts b/server/src/services/chat-channels/discord/command-work.ts index 28169037824..af826d9cf1b 100644 --- a/server/src/services/chat-channels/discord/command-work.ts +++ b/server/src/services/chat-channels/discord/command-work.ts @@ -88,7 +88,9 @@ export function registerDiscordCommandWork( if (due.length > batch.entries.length) earliest = Date.now() + 1_000; if (errors.length) throw new AggregateError(errors, "Discord command registration could not settle"); }, - hasPending: async () => dirty.size > 0, + // Preserve hints/deadlines for resume without holding settled intent during drain. + // The coordinator independently fences unresolved database transactions. + hasPending: async () => canRun() && dirty.size > 0, nextRunAt: next, }); } diff --git a/server/src/services/chat-discord-command-registration-store.test.ts b/server/src/services/chat-discord-command-registration-store.test.ts index 71660aaeae6..44809957ca4 100644 --- a/server/src/services/chat-discord-command-registration-store.test.ts +++ b/server/src/services/chat-discord-command-registration-store.test.ts @@ -765,7 +765,9 @@ suite("Discord command ownership store (real PostgreSQL, no network)", () => { await sqlQuiet(60_000); draining = true; await db.transaction(async tx => activate(tx as unknown as typeof db)); - await sqlQuiet(90_000); expect(f.calls).toEqual([]); + await vi.waitFor(async () => { await vi.advanceTimersByTimeAsync(1000); expect(idleWorkSnapshot().active).toBe(0); }); + const drained = idleWorkSnapshot(); + await sqlQuiet(90_000); expect(idleWorkSnapshot()).toEqual(drained); expect(f.calls).toEqual([]); draining = false; await vi.advanceTimersByTimeAsync(30_000); await vi.waitFor(() => expect(f.calls).toEqual(["GET", "POST"]));