diff --git a/backend/__tests__/unit/services/kernelWorkSweepService.test.js b/backend/__tests__/unit/services/kernelWorkSweepService.test.js index 5c1a9d371c..e9e928a28d 100644 --- a/backend/__tests__/unit/services/kernelWorkSweepService.test.js +++ b/backend/__tests__/unit/services/kernelWorkSweepService.test.js @@ -329,7 +329,7 @@ describe('sweep', () => { { _id: POD_A, tasks: [{ taskId: 'TASK-007', title: 'stalled work' }], count: 1 }, ]); - const result = await KernelWorkSweepService.sweep(NOW); + const result = await KernelWorkSweepService.wakeForFoundWork(NOW); expect(mockNotifyFoundWork).toHaveBeenCalledTimes(1); const [podId, items, count] = mockNotifyFoundWork.mock.calls[0]; @@ -342,7 +342,7 @@ describe('sweep', () => { it('matches unoffered revisions regardless of age so the hourly batch can retry', async () => { Task.aggregate.mockResolvedValue([]); - await KernelWorkSweepService.sweep(NOW); + await KernelWorkSweepService.wakeForFoundWork(NOW); const [pipeline] = Task.aggregate.mock.calls[0]; const match = pipeline[0].$match; @@ -370,7 +370,7 @@ describe('sweep', () => { })); Task.aggregate.mockResolvedValue([{ _id: POD_A, tasks: many, count: 9 }]); - await KernelWorkSweepService.sweep(NOW); + await KernelWorkSweepService.wakeForFoundWork(NOW); const [, items, count] = mockNotifyFoundWork.mock.calls[0]; expect(items).toHaveLength(9); @@ -429,14 +429,25 @@ test('watcher receipts retry even when work discovery is disabled', async () => }); -test('a watcher retry failure does not block rescue or work discovery', async () => { +test('a watcher retry failure does not block rescue', async () => { const receipts = require('../../../services/taskEventService').retryTaskWatcherDigests; receipts.mockRejectedValueOnce(new Error('lookup failed')); const rescue = jest.spyOn(KernelWorkSweepService, 'rescueLapsed'); - const discovery = jest.spyOn(KernelWorkSweepService, 'wakeForFoundWork'); try { await KernelWorkSweepService.sweep(NOW); expect(rescue).toHaveBeenCalledTimes(1); - expect(discovery).toHaveBeenCalledTimes(1); - } finally { rescue.mockRestore(); discovery.mockRestore(); } + } finally { rescue.mockRestore(); } +}); + +test('TASK-381: the sweep sends no found-work nudge, even with unclaimed rows due', async () => { + Task.aggregate.mockResolvedValue([ + { _id: POD_A, tasks: [{ taskId: 'TASK-015', title: 'decouple' }], count: 1 }, + ]); + const discovery = jest.spyOn(KernelWorkSweepService, 'wakeForFoundWork'); + try { + const result = await KernelWorkSweepService.sweep(NOW); + expect(discovery).not.toHaveBeenCalled(); + expect(mockNotifyFoundWork).not.toHaveBeenCalled(); + expect(result.woken).toBe(0); + } finally { discovery.mockRestore(); } }); diff --git a/backend/services/kernelWorkSweepService.ts b/backend/services/kernelWorkSweepService.ts index 98bbeb278d..b77508da2a 100644 --- a/backend/services/kernelWorkSweepService.ts +++ b/backend/services/kernelWorkSweepService.ts @@ -365,13 +365,17 @@ class KernelWorkSweepService { scannedPods: 0, rescued: 0, deferred: 0, woken: 0, skippedNoWork: 0, }; } + // TASK-381 (ruling 2026-10-27): the hourly found-work nudge is retired. It + // woke every implementer seat for unclaimed rows, and most of those wakes + // ended NO_REPLY. Unclaimed rows reach a seat through the row router (one + // PATCH + one @mention per row); assigned rows through the assigned-task + // wake. So the sweep no longer calls wakeForFoundWork. const { rescued, deferred } = await KernelWorkSweepService.rescueLapsed(now); - const { woken, skippedNoWork, scannedPods } = await KernelWorkSweepService.wakeForFoundWork(now); - if (rescued.length || deferred.length || woken) { - console.log(`[kernel-sweep] rescued=${rescued.length} deferred=${deferred.length} woken=${woken} pods=${scannedPods}`); + if (rescued.length || deferred.length) { + console.log(`[kernel-sweep] rescued=${rescued.length} deferred=${deferred.length}`); } return { - scannedPods, rescued: rescued.length, deferred: deferred.length, woken, skippedNoWork, + scannedPods: 0, rescued: rescued.length, deferred: deferred.length, woken: 0, skippedNoWork: 0, }; } } diff --git a/docs/adr/ADR-018-agent-attention-claims.md b/docs/adr/ADR-018-agent-attention-claims.md index 8ebd338129..28786208d8 100644 --- a/docs/adr/ADR-018-agent-attention-claims.md +++ b/docs/adr/ADR-018-agent-attention-claims.md @@ -473,6 +473,12 @@ The D8.2 discovery exception woke reviewers who had no implementation capacity: Discovery now requires an explicit `config.workDiscovery: { enabled: true, role: 'implementer' }` on the installation. It skips a seat with assigned pending work or an active claim in that pod, whether the claim stores a seat key or its bot User id, and caps delivery at one nudge per seat and pod per hour. Missing role configuration fails closed. An unoffered revision remains eligible beyond the sweep's ten-minute tick so the next hourly batch can carry it. A capped nudge does not mark an unassigned row as offered. Browser board updates and addressed owner wakes are unaffected. +### D8.7 — AMENDMENT (2026-10-27): the kernel sweep no longer sends the found-work nudge (TASK-381) + +Lily ruled on TASK-381 (decision `6ae0493258eabb3645e350c3`): narrow the found-work wake to assigned or addressed events. `KernelWorkSweepService.sweep()` no longer calls `wakeForFoundWork`, so the hourly D8.3 nudge is not sent by the sweep. Unclaimed rows reach a seat through the row router (one assignment and one @mention per row). Assigned rows keep the assigned-task wake (D8.4), and addressed owner wakes are unaffected. + +What this leaves in place, so the next reader does not assume it is gone: `wakeForFoundWork` and `notifyFoundWork` still exist and are still exercised by direct tests. Nothing in production reaches them from the sweep. The `config.workDiscovery` read in D8.3 is therefore inert until those functions are either deleted or reconnected, and that choice is pending. Until it is made, read D8.3 as describing behaviour the sweep no longer performs. + ### D8.4 — AMENDMENT (2026-10-24): assigned work survives a silent turn **Status: ACCEPTED direction (TASK-469), including the ruling that deliberate