From 1558d9962ca9236705981defd07601573ce0ebd6 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Sun, 18 Oct 2026 13:56:08 -0700 Subject: [PATCH] fix(idle): exclude completed feedback history from background work (#16764) Allow private feedback history and acknowledged uploads to stop blocking idle sleep. Pending, failed, unfamiliar, and inconsistent delivery records remain blocked; preserve all other work and cleanup gates. Verified with 220 idle-safety tests, workspace typecheck/build, full CI, and Greptile Apex 5/5 on the reviewed head. Local full-suite limitations and isolated rechecks are documented in the PR. Co-Authored-By: Paperclip --- doc/DATABASE.md | 8 ++++ .../src/__tests__/idle-sleep-safety.test.ts | 37 +++++++++++++++++++ server/src/services/idle-sleep-safety.ts | 7 +++- 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/doc/DATABASE.md b/doc/DATABASE.md index c0ce8f04e52..7bca07fbdf8 100644 --- a/doc/DATABASE.md +++ b/doc/DATABASE.md @@ -411,6 +411,14 @@ Rollback by setting the marker to `0` and restarting. No schema changes are required. This mechanism does not sleep claimed workspaces or replace a durable scheduler for their background work. +## Feedback history and idle sleep + +Automatic idle checks treat private, unshared feedback as saved data. A sent +feedback export is history only when its completion timestamp exists and its +failure reason is clear. Pending, failed, unknown, and inconsistent exports +still keep the instance awake. These checks do not delete history or change +feedback sharing and retry behavior. + ## Resource membership tables Paperclip stores current-user sidebar membership state in: diff --git a/server/src/__tests__/idle-sleep-safety.test.ts b/server/src/__tests__/idle-sleep-safety.test.ts index befce2fac67..eb5ae4230b3 100644 --- a/server/src/__tests__/idle-sleep-safety.test.ts +++ b/server/src/__tests__/idle-sleep-safety.test.ts @@ -5,6 +5,7 @@ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest import { agentInstructionWorkingCopies, agents, agentApiKeys, agentWakeupRequests, companies, companySecretProposals, createDb, adapterAuthSessions, environments, environmentLeases, executionWorkspaces, + feedbackExports, feedbackVotes, heartbeatRuns, issues, issueWatchdogs, issueThreadInteractions, issueQuestionResponseDeliveries, projects, projectWorkspaces, routines, routineTriggers, toolApplications, toolConnections, workspaceOperations, executionWorkspaceRuntimeLeases, type Db, } from "@paperclipai/db"; @@ -186,6 +187,42 @@ if (!support.supported) console.warn(`Skipping idle sleep Postgres tests: ${supp return { companyId, agentId, runId }; } + async function feedbackHistory(status: string, change: Partial = {}) { + const f = await seed(); + const [issue] = await db.insert(issues).values({ companyId: f.companyId, title: "Finished fixture", status: "done" }).returning(); + const [vote] = await db.insert(feedbackVotes).values({ companyId: f.companyId, issueId: issue.id, + targetType: "issue", targetId: issue.id, authorUserId: "fixture-user", vote: "up" }).returning(); + await db.insert(feedbackExports).values({ companyId: f.companyId, feedbackVoteId: vote.id, + issueId: issue.id, authorUserId: "fixture-user", targetType: "issue", targetId: issue.id, + vote: "up", status, targetSummary: {}, ...change }); + } + + it("permits saved private feedback without sharing or deleting its history", async () => { + await feedbackHistory("local_only"); + expect(await read()).toEqual(none); + expect(await db.select({ status: feedbackExports.status }).from(feedbackExports)).toEqual([{ status: "local_only" }]); + }); + + it("permits an acknowledged feedback upload and preserves its receipt", async () => { + await feedbackHistory("sent", { exportedAt: new Date(now), failureReason: null }); + expect(await read()).toEqual(none); + expect(await db.select({ status: feedbackExports.status, exportedAt: feedbackExports.exportedAt }).from(feedbackExports)) + .toEqual([{ status: "sent", exportedAt: new Date(now) }]); + }); + + it.each(["pending", "failed", "unfamiliar"])("keeps %s feedback work awake even with an old completion stamp", async status => { + await feedbackHistory(status, { exportedAt: new Date(now), failureReason: null }); + expect(await read()).toEqual(present); + }); + + it.each([ + { exportedAt: null, failureReason: null }, + { exportedAt: new Date(now), failureReason: "unconfirmed delivery" }, + ])("keeps inconsistent sent feedback awake: %j", async change => { + await feedbackHistory("sent", change); + expect(await read()).toEqual(present); + }); + async function scheduledRoutine(at = now + 600_000) { const fixture = await seed(); const [routine] = await db.insert(routines).values({ companyId: fixture.companyId, diff --git a/server/src/services/idle-sleep-safety.ts b/server/src/services/idle-sleep-safety.ts index 92441010f61..11c228f1ef3 100644 --- a/server/src/services/idle-sleep-safety.ts +++ b/server/src/services/idle-sleep-safety.ts @@ -227,6 +227,11 @@ const WORK_CHECKS = [ 'connection-intent:' || d.interaction_id::text || ':rejected', 'connection-intent:' || d.interaction_id::text || ':expired') AND (${finishedIdleWake('connection_wake')}) IS NOT TRUE)`, + // Saved private feedback and acknowledged uploads have no delivery job. + // Pending/failed exports and unfamiliar states retain the instance. A sent + // label requires its completion stamp and a cleared failure reason. + `SELECT 1 FROM feedback_exports WHERE (status = 'local_only' + OR (status = 'sent' AND exported_at IS NOT NULL AND failure_reason IS NULL)) IS NOT TRUE`, // These less common work sources fail closed on any retained state. Their // terminal-state exceptions can be added with tests for the owning service. ...[ @@ -236,7 +241,7 @@ const WORK_CHECKS = [ "execution_workspace_runtime_leases", "pipeline_automation_executions", "native_run_finalizations", "company_transfer_runs", "decisions", "decision_effect_executions", "decision_archive_notification_outbox", "browser_use_sessions", "browser_use_runs", - "browser_use_browsers", "feedback_exports", + "browser_use_browsers", "company_secret_proposals", "mcp_oauth_grants", "mcp_mutation_receipts", "mcp_event_subscriptions", "mcp_event_deliveries", "mcp_attachment_uploads", "dot_agent_bindings",