diff --git a/doc/idle-sleep-safety.md b/doc/idle-sleep-safety.md index 5b425870898..8cf330272b2 100644 --- a/doc/idle-sleep-safety.md +++ b/doc/idle-sleep-safety.md @@ -386,6 +386,11 @@ settles, including after the raw socket closes. Detached startup reconciliation for built-in agents, managed homes and persisted runtime services also remains counted after readiness until its writes settle. +Retained native response presentation also continues after web readiness. Its +fair scheduler and persisted retry deadlines remain in effect; in-flight work +stays counted for idle and is joined at shutdown. Web readiness does not require +the retained reply queue to become idle. + Scheduler work already in flight remains counted until its promise settles. Idle holds pause new scheduler admissions. Database backup promises remain counted through success or failure. Configured periodic backups block sleep by diff --git a/server/src/__tests__/server-startup-feedback-export.test.ts b/server/src/__tests__/server-startup-feedback-export.test.ts index 49877b828a5..6c367a02f5b 100644 --- a/server/src/__tests__/server-startup-feedback-export.test.ts +++ b/server/src/__tests__/server-startup-feedback-export.test.ts @@ -1705,7 +1705,7 @@ describe("startServer feedback export wiring", () => { } finally { release(); await starting; timer.mockRestore(); } }); - it.each(["retained cleanup", "workspace finalization", "environment settlement", "native run projection", "native result commit", "native recovery notices", "external conversation settlement"] as const)("becomes ready while %s is held but joins it at shutdown", async owner => { + it.each(["retained cleanup", "workspace finalization", "environment settlement", "native run projection", "native result commit", "native recovery notices", "external conversation settlement", "native response presentation"] as const)("becomes ready while %s is held but joins it at shutdown", async owner => { loadConfigMock.mockReturnValue(buildTestConfig({ heartbeatSchedulerEnabled: true })); let releaseCleanup!: () => void; const ready = new Promise(resolve => { releaseCleanup = resolve; }); @@ -1733,11 +1733,13 @@ describe("startServer feedback export wiring", () => { : owner === "native run projection" ? heartbeatServiceMock.registerNativeRunProjectionWork : owner === "native result commit" ? heartbeatServiceMock.registerNativeResultCommitWork : owner === "native recovery notices" ? heartbeatServiceMock.registerNativeRecoveryNoticeWork + : owner === "native response presentation" ? heartbeatServiceMock.registerNativeResponsePresentationWork : heartbeatServiceMock.registerExternalConversationSettlementWork; if (owner === "native run projection") heartbeatServiceMock.registerNativeRunProjectionWork.mockReturnValueOnce({ ready }); else if (owner === "native result commit") heartbeatServiceMock.registerNativeResultCommitWork.mockReturnValueOnce({ ready }); else if (owner === "native recovery notices") heartbeatServiceMock.registerNativeRecoveryNoticeWork.mockReturnValueOnce({ ready }); else if (owner === "external conversation settlement") heartbeatServiceMock.registerExternalConversationSettlementWork.mockReturnValueOnce({ ready }); + else if (owner === "native response presentation") heartbeatServiceMock.registerNativeResponsePresentationWork.mockReturnValueOnce({ ready, stop }); else (owner === "retained cleanup" ? heartbeatServiceMock.registerRetainedNativeSessionCleanupWork : owner === "workspace finalization" ? heartbeatServiceMock.registerNativeWorkspaceFinalizationWork : heartbeatServiceMock.registerNativeEnvironmentSettlementWork).mockReturnValueOnce({ ready, stop }); @@ -1751,6 +1753,7 @@ describe("startServer feedback export wiring", () => { await settleStartupHousekeeping(); expect(started).toBe(true); expect(getStartupRecoveryState().phase).toBe("ready"); + if (owner === "native response presentation") expect(idleWorkSnapshot().active).toBeGreaterThan(0); expect(heartbeatServiceMock.registerNativeStatusReassessmentWork).toHaveBeenCalledOnce(); const server = await starting; stopping = server.shutdown().then(() => { stopped = true; }); @@ -1761,9 +1764,14 @@ describe("startServer feedback export wiring", () => { expect(onRelease).toHaveBeenCalledOnce(); } expect(stopped).toBe(false); + if (owner === "native response presentation") { + expect(stop).toHaveBeenCalled(); + expect(idleWorkSnapshot().active).toBeGreaterThan(0); + } releaseCleanup(); await stopping; expect(stopped).toBe(true); + if (owner === "native response presentation") expect(idleWorkSnapshot().active).toBe(0); if (releaseNotification) { releaseNotification(); expect(onRelease).toHaveBeenCalledOnce(); diff --git a/server/src/index.ts b/server/src/index.ts index a962af47685..17466c5168c 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1812,7 +1812,7 @@ async function startServerWithDatabaseTeardown( () => !isIdleTaskDrainActive() && !heartbeatSchedulerStopped && !isWarmStandby()); unsubscribeNativeResponsePresentation = worker.stop; server.on("close", worker.stop); - await worker.ready; + trackHeartbeatSchedulerWork(worker.ready); } if (config.heartbeatSchedulerEnabled) await heartbeat.registerNativePendingEffectWork(deliveryWork, () => !isIdleTaskDrainActive() && !heartbeatSchedulerStopped && !isWarmStandby()).ready;