From e685a0e7ef962071f26d072b433e72a0ee27f5ed Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 25 Oct 2026 09:55:19 -0500 Subject: [PATCH] fix(server): release settled tool health intent during drain Co-Authored-By: Paperclip --- ...10-25-configuration-worker-drain-repair.md | 31 +++++++++++++++++++ .../tool-connection-health-work.test.ts | 5 ++- .../services/tool-connection-health-work.ts | 4 ++- 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 doc/plans/2026-10-25-configuration-worker-drain-repair.md diff --git a/doc/plans/2026-10-25-configuration-worker-drain-repair.md b/doc/plans/2026-10-25-configuration-worker-drain-repair.md new file mode 100644 index 00000000000..99ebdb41834 --- /dev/null +++ b/doc/plans/2026-10-25-configuration-worker-drain-repair.md @@ -0,0 +1,31 @@ +# Configuration worker drain repairs + +The voice drain audit exposed a pattern in three earlier independent workers: +retained dirty scopes reported as pending work keep the coordinator's intent +activity hold alive after the write has settled. No recurring SQL is necessary +to block idle admission this way. The earlier tests checked traffic but did not +check the idle counter or generation while paused. + +Each owner must retain its hints and next deadline for resume, report dirty work +as pending only while dispatch is allowed, and leave unresolved transaction +fencing to the shared coordinator. No hint is acknowledged or discarded during +drain. Business health/registration/lease deadlines and provider behavior stay +unchanged. This is an idle-work boundary repair, not provider sleep qualification. + +## Tool health — slice 18 / #17554 + +Baseline 9ef6e4f4c8 reproduces the bug: the strengthened existing drain case fails +with active=1 after commit, recorded in /tmp/sleep18-drain-before.log. The repaired +case asserts active=0, then a stable idle snapshot and no SQL/provider calls for a +minute, then the saved work runs on resume without another producer. + +All 23 lifecycle cases pass in /tmp/sleep18-drain-tests.log; production server +typecheck passes in /tmp/sleep18-drain-typecheck.log. The test database is the +fresh migrated paperclip_sleep18_drain_20261025_test. No test failures were retried +blindly. Current master bf9a8551c3 has no changes to these three worker owners +since the preceding aec86c794b inspection; full stack reconciliation remains open. + +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. diff --git a/server/src/__tests__/tool-connection-health-work.test.ts b/server/src/__tests__/tool-connection-health-work.test.ts index 247f3c36873..da7b53eacbe 100644 --- a/server/src/__tests__/tool-connection-health-work.test.ts +++ b/server/src/__tests__/tool-connection-health-work.test.ts @@ -185,8 +185,11 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu }); it("retains first writes through drain without querying", async () => { await start(); await quiet(); draining = true; await connection(); + await vi.waitFor(async () => { await vi.advanceTimersByTimeAsync(1000); expect(idleWorkSnapshot().active).toBe(0); }); + const drained = idleWorkSnapshot(); const reads = vi.spyOn(db, "select"); await vi.advanceTimersByTimeAsync(60_000); - expect(reads).not.toHaveBeenCalled(); reads.mockRestore(); expect(probe).not.toHaveBeenCalled(); + expect(reads).not.toHaveBeenCalled(); expect(idleWorkSnapshot()).toEqual(drained); + reads.mockRestore(); expect(probe).not.toHaveBeenCalled(); draining = false; await checked(); }); it("moves the deadline when an actual-use health observation commits", async () => { diff --git a/server/src/services/tool-connection-health-work.ts b/server/src/services/tool-connection-health-work.ts index 6d684ba4a42..7259baca19e 100644 --- a/server/src/services/tool-connection-health-work.ts +++ b/server/src/services/tool-connection-health-work.ts @@ -82,7 +82,9 @@ export function registerToolConnectionHealthWork( if (due.length > batch.entries.length) earliest = Date.now() + 1_000; if (errors.length) throw new AggregateError(errors, "Tool connection health checks could not settle"); }, - hasPending: async () => dirty.size > 0, + // Keep the hints/deadlines for resume, but release settled intent while + // paused. The coordinator independently fences unresolved transactions. + hasPending: async () => canRun() && dirty.size > 0, nextRunAt: next, }); }