mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-07 07:55:47 +01:00
feat(server): expose owned sleep blocker diagnostics
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
0748b90062
commit
4c4e4d3135
11 files changed
+805
-126
No files matched your search
@@ -2,6 +2,7 @@ import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { resolvePaperclipInstanceRoot } from "../home-paths.js";
|
||||
import { idleWorkSnapshot } from "./task-admission.js";
|
||||
import type { IdleSleepDiagnosticSink } from "./idle-sleep-diagnostics.js";
|
||||
|
||||
export type IdleLocalWork = "none" | "present" | "unknown";
|
||||
const spoolDirectories = new Set<string>();
|
||||
@@ -61,21 +62,50 @@ export async function inspectIdleQuotaRecovery(directory: string): Promise<IdleL
|
||||
}
|
||||
}
|
||||
|
||||
export async function readIdleLocalWork(options: { backupCheckpoint?: boolean } = {}): Promise<IdleLocalWork> {
|
||||
if (!startupComplete || !ingressTracked) return "unknown";
|
||||
export async function readIdleLocalWork(options: { backupCheckpoint?: boolean; diagnostic?: IdleSleepDiagnosticSink } = {}): Promise<IdleLocalWork> {
|
||||
const record = options.diagnostic;
|
||||
const states: IdleLocalWork[] = [];
|
||||
record?.("local.startup", startupComplete ? "none" : "unknown");
|
||||
record?.("local.ingress_tracking", ingressTracked ? "none" : "unknown");
|
||||
if (!startupComplete || !ingressTracked) {
|
||||
if (!record) return "unknown";
|
||||
states.push("unknown");
|
||||
}
|
||||
// This exemption belongs only to a scan which will finish a new backup
|
||||
// under the same admission hold before it can return an empty report.
|
||||
if ((scheduledBackups && !options.backupCheckpoint) || idleWorkSnapshot().active !== 0) return "present";
|
||||
const backup = scheduledBackups && !options.backupCheckpoint;
|
||||
const active = idleWorkSnapshot().active !== 0;
|
||||
record?.("local.automatic_backups", backup ? "present" : "none");
|
||||
record?.("local.tracked_work", active ? "present" : "none");
|
||||
if (backup || active) {
|
||||
if (!record) return "present";
|
||||
states.push("present");
|
||||
}
|
||||
let probes: IdleLocalWork;
|
||||
try {
|
||||
const local = [...localWorkProbes].map(probe => probe());
|
||||
if (local.includes("unknown")) return "unknown";
|
||||
if (local.includes("present")) return "present";
|
||||
} catch { return "unknown"; }
|
||||
const directories = new Set([...spoolDirectories, idleAccountingSpoolPath(), idleOrphanSpoolPath()]);
|
||||
probes = local.some(state => state !== "none" && state !== "present") ? "unknown"
|
||||
: local.includes("present") ? "present" : "none";
|
||||
} catch { probes = "unknown"; }
|
||||
record?.("local.registered_probes", probes);
|
||||
if (probes !== "none" && !record) return probes;
|
||||
states.push(probes);
|
||||
const accounting = idleAccountingSpoolPath();
|
||||
const orphan = idleOrphanSpoolPath();
|
||||
const directories = new Set([...spoolDirectories, accounting, orphan]);
|
||||
const results = await Promise.all([
|
||||
...[...directories].map(inspectIdleSpool),
|
||||
inspectIdleQuotaRecovery(idleQuotaRecoveryPath()),
|
||||
...[...directories].map(async directory => {
|
||||
const state = await inspectIdleSpool(directory);
|
||||
record?.(directory === accounting ? "local.accounting_spool"
|
||||
: directory === orphan ? "local.orphan_spool" : "local.registered_spools", state);
|
||||
return state;
|
||||
}),
|
||||
inspectIdleQuotaRecovery(idleQuotaRecoveryPath()).then(state => {
|
||||
record?.("local.quota_recovery", state);
|
||||
return state;
|
||||
}),
|
||||
]);
|
||||
if (results.includes("unknown")) return "unknown";
|
||||
return results.includes("present") ? "present" : "none";
|
||||
states.push(...results);
|
||||
if (states.includes("unknown")) return "unknown";
|
||||
return states.includes("present") ? "present" : "none";
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
export type IdleSleepCheckState = "none" | "present" | "unknown" | "not_checked";
|
||||
export type IdleSleepDiagnosticSink = (reason: string, state: IdleSleepCheckState) => void;
|
||||
|
||||
export interface IdleSleepDiagnostics {
|
||||
version: 1;
|
||||
complete: boolean;
|
||||
checks: { reason: string; state: IdleSleepCheckState }[];
|
||||
}
|
||||
|
||||
export const IDLE_LOCAL_DIAGNOSTIC_REASONS = [
|
||||
"local.startup", "local.ingress_tracking", "local.automatic_backups", "local.tracked_work",
|
||||
"local.registered_probes", "local.accounting_spool", "local.orphan_spool",
|
||||
"local.registered_spools", "local.quota_recovery",
|
||||
] as const;
|
||||
|
||||
export function createIdleSleepDiagnostics() {
|
||||
const states = new Map<string, IdleSleepCheckState>();
|
||||
const allowed = new Set<string>([
|
||||
...IDLE_LOCAL_DIAGNOSTIC_REASONS,
|
||||
"inspection.hold", "inspection.generation", "durable.snapshot", "local.inspection",
|
||||
"plugins.runtime_drain", "routines.schedule", "issues.schedule", "backup.checkpoint",
|
||||
]);
|
||||
const rank = { none: 0, not_checked: 1, present: 2, unknown: 3 } as const;
|
||||
const record: IdleSleepDiagnosticSink = (reason, state) => {
|
||||
if (!allowed.has(reason) || !Object.hasOwn(rank, state)) throw new Error("Invalid idle diagnostic check");
|
||||
const previous = states.get(reason);
|
||||
if (previous === undefined || rank[state] > rank[previous]) states.set(reason, state);
|
||||
};
|
||||
return {
|
||||
record,
|
||||
expect(reasons: string[]) {
|
||||
for (const reason of reasons) {
|
||||
if (allowed.has(reason)) throw new Error("Duplicate idle diagnostic reason");
|
||||
allowed.add(reason);
|
||||
}
|
||||
},
|
||||
report(): IdleSleepDiagnostics {
|
||||
return {
|
||||
version: 1,
|
||||
complete: [...states].every(([reason, state]) => reason === "backup.checkpoint"
|
||||
|| (state !== "unknown" && state !== "not_checked")),
|
||||
checks: [...states].sort(([a], [b]) => a.localeCompare(b)).map(([reason, state]) => ({ reason, state })),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -4,7 +4,8 @@ import { readIdleRoutineSchedule, type ScheduledWake } from "./idle-scheduled-wo
|
||||
import { combineIdleSchedules, readIdleIssueSchedule } from "./idle-issue-schedule.js";
|
||||
import { connectionDeliveryWaitingForResponse, finishedIdleRun as finishedRun, finishedIdleWake, finishedFailedIdleLease as finishedFailedLease, passiveBoardRecovery } from "./idle-work-proof.js";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { sql, type SQLWrapper } from "drizzle-orm";
|
||||
import { createIdleSleepDiagnostics, type IdleSleepDiagnosticSink, type IdleSleepDiagnostics } from "./idle-sleep-diagnostics.js";
|
||||
import { verifiedResourceCleanup } from "./cleanup-verification-proof.js";
|
||||
import { companyHoldMismatch } from "../modules/agent-lifecycle/index.js";
|
||||
import { pendingBudgetRunCancellationPredicate } from "./heartbeat/budget-cancellation.js";
|
||||
@@ -23,6 +24,7 @@ export interface IdleSleepSafety {
|
||||
version: 1 | 2;
|
||||
backgroundWork: "none" | "present" | "unknown";
|
||||
scheduledWake?: ScheduledWake;
|
||||
diagnostics?: IdleSleepDiagnostics;
|
||||
}
|
||||
|
||||
export type InspectIdlePlugins = (hold: { ownerId: string; expiresAt: number }) => Promise<{
|
||||
@@ -40,50 +42,52 @@ export type InspectIdlePlugins = (hold: { ownerId: string; expiresAt: number })
|
||||
// Other background features remain awake until their work and inbound events
|
||||
// have a durable wake path. In particular, an empty in-memory run set is not
|
||||
// evidence that a restarted process has no persisted work to recover.
|
||||
const workCheck = (reason: string, query: string | SQLWrapper) => ({ reason, query: typeof query === "string" ? sql.raw(query) : query });
|
||||
|
||||
const WORK_CHECKS = [
|
||||
`SELECT 1 FROM agents WHERE status = 'running' OR
|
||||
workCheck("agents.execution_or_timer", `SELECT 1 FROM agents WHERE status = 'running' OR
|
||||
(status NOT IN ('terminated', 'paused', 'pending_approval') AND
|
||||
runtime_config->'heartbeat'->>'enabled' = 'true' AND
|
||||
COALESCE((runtime_config->'heartbeat'->>'intervalSec')::numeric, 0) > 0)`,
|
||||
`SELECT 1 FROM heartbeat_runs WHERE
|
||||
COALESCE((runtime_config->'heartbeat'->>'intervalSec')::numeric, 0) > 0)`),
|
||||
workCheck("heartbeat_runs", `SELECT 1 FROM heartbeat_runs WHERE
|
||||
status NOT IN ('succeeded', 'failed', 'cancelled', 'timed_out', 'interrupted')
|
||||
OR (scheduled_retry_at IS NOT NULL
|
||||
AND (finished_at IS NULL OR scheduled_retry_at > finished_at))
|
||||
OR cost_accounting_pending`,
|
||||
`SELECT 1 FROM agent_instruction_working_copies WHERE
|
||||
OR cost_accounting_pending`),
|
||||
workCheck("agent_instruction_working_copies", `SELECT 1 FROM agent_instruction_working_copies WHERE
|
||||
state NOT IN ('saved', 'unchanged', 'resolved', 'unavailable', 'conflict')
|
||||
OR process_stopped_at IS NULL OR next_attempt_at IS NOT NULL
|
||||
OR receipt ? 'baseline' OR receipt->>'cleanupPending' = 'true'`,
|
||||
`SELECT 1 FROM sandbox_creation_operations WHERE state = 'pending'`,
|
||||
`SELECT 1 FROM budget_reservations WHERE state NOT IN ('settled', 'released')`,
|
||||
`SELECT 1 FROM budget_policies WHERE enforcement_version > enforcement_delivered_version`,
|
||||
`SELECT 1 FROM agents WHERE lifecycle_state IN
|
||||
('preparing', 'verifying', 'pausing', 'resuming', 'terminating', 'cleaning_up')`,
|
||||
`SELECT 1 FROM decision_invocations WHERE status = 'running'`,
|
||||
`SELECT 1 FROM prompt_injection_invocations WHERE status = 'running'`,
|
||||
OR receipt ? 'baseline' OR receipt->>'cleanupPending' = 'true'`),
|
||||
workCheck("sandbox_creation_operations", `SELECT 1 FROM sandbox_creation_operations WHERE state = 'pending'`),
|
||||
workCheck("budget_reservations", `SELECT 1 FROM budget_reservations WHERE state NOT IN ('settled', 'released')`),
|
||||
workCheck("budget_policies", `SELECT 1 FROM budget_policies WHERE enforcement_version > enforcement_delivered_version`),
|
||||
workCheck("agents.lifecycle", `SELECT 1 FROM agents WHERE lifecycle_state IN
|
||||
('preparing', 'verifying', 'pausing', 'resuming', 'terminating', 'cleaning_up')`),
|
||||
workCheck("decision_invocations", `SELECT 1 FROM decision_invocations WHERE status = 'running'`),
|
||||
workCheck("prompt_injection_invocations", `SELECT 1 FROM prompt_injection_invocations WHERE status = 'running'`),
|
||||
// Persistent machine credentials can receive work without a human opening
|
||||
// the instance. They need an ingress wake contract before we can sleep.
|
||||
`SELECT 1 FROM agent_api_keys WHERE revoked_at IS NULL`,
|
||||
`SELECT 1 FROM board_api_keys WHERE revoked_at IS NULL
|
||||
AND (expires_at IS NULL OR expires_at > now())`,
|
||||
workCheck("agent_api_keys", `SELECT 1 FROM agent_api_keys WHERE revoked_at IS NULL`),
|
||||
workCheck("board_api_keys", `SELECT 1 FROM board_api_keys WHERE revoked_at IS NULL
|
||||
AND (expires_at IS NULL OR expires_at > now())`),
|
||||
// Coalescing consumes a request into another run. Retain it as work until
|
||||
// both the request and that exact company/agent's run have finished.
|
||||
`SELECT 1 FROM agent_wakeup_requests w WHERE
|
||||
workCheck("agent_wakeup_requests", `SELECT 1 FROM agent_wakeup_requests w WHERE
|
||||
w.status NOT IN ('completed', 'failed', 'cancelled', 'skipped', 'timed_out', 'coalesced')
|
||||
OR (w.status = 'coalesced' AND (w.finished_at IS NULL OR NOT EXISTS (
|
||||
SELECT 1 FROM heartbeat_runs r WHERE r.id = w.run_id
|
||||
AND r.company_id = w.company_id AND r.agent_id = w.agent_id
|
||||
AND ${finishedRun("r")})))`,
|
||||
`SELECT 1 FROM issues WHERE status NOT IN ('done', 'cancelled')`,
|
||||
AND ${finishedRun("r")})))`),
|
||||
workCheck("issues", `SELECT 1 FROM issues WHERE status NOT IN ('done', 'cancelled')`),
|
||||
// A completed issue can still need its first watchdog review, including a
|
||||
// retry after immediate evaluation failed before creating a review/run.
|
||||
`SELECT 1 FROM issue_watchdogs WHERE status <> 'disabled'`,
|
||||
`SELECT 1 FROM workspace_operations WHERE status NOT IN ('succeeded', 'failed', 'cancelled')`,
|
||||
workCheck("issue_watchdogs", `SELECT 1 FROM issue_watchdogs WHERE status <> 'disabled'`),
|
||||
workCheck("workspace_operations", `SELECT 1 FROM workspace_operations WHERE status NOT IN ('succeeded', 'failed', 'cancelled')`),
|
||||
// A local directory has no independent worker. Keep unfamiliar providers,
|
||||
// runtime/command configuration, reopen fences and due cleanup awake. Runs,
|
||||
// operations, runtime services, retained leases and backup work are checked
|
||||
// separately; inspecting the row never deletes or shortens retained data.
|
||||
`SELECT 1 FROM execution_workspaces w WHERE (
|
||||
workCheck("execution_workspaces", `SELECT 1 FROM execution_workspaces w WHERE (
|
||||
w.provider_type IN ('local_fs', 'git_worktree')
|
||||
AND w.strategy_type IN ('project_primary', 'git_worktree')
|
||||
AND w.mode IN ('inherit', 'shared_workspace', 'isolated_workspace', 'operator_branch', 'reuse_existing', 'agent_default')
|
||||
@@ -107,16 +111,16 @@ const WORK_CHECKS = [
|
||||
AND COALESCE(w.metadata->'config'->>'runtimeProvisionCommand', '') = ''
|
||||
AND COALESCE(w.metadata->'config'->>'teardownCommand', '') = ''
|
||||
AND COALESCE(w.metadata->'config'->>'cleanupCommand', '') = ''))
|
||||
) IS NOT TRUE`,
|
||||
`SELECT 1 FROM environment_leases l WHERE status NOT IN ('released', 'expired')
|
||||
AND (${finishedFailedLease("l")}) IS NOT TRUE`,
|
||||
`SELECT 1 FROM environment_custom_image_setup_sessions s WHERE
|
||||
) IS NOT TRUE`),
|
||||
workCheck("environment_leases", `SELECT 1 FROM environment_leases l WHERE status NOT IN ('released', 'expired')
|
||||
AND (${finishedFailedLease("l")}) IS NOT TRUE`),
|
||||
workCheck("environment_custom_image_setup_sessions", `SELECT 1 FROM environment_custom_image_setup_sessions s WHERE
|
||||
(s.status IN ('promoted', 'cancelled', 'timed_out', 'failed') AND s.finished_at IS NOT NULL
|
||||
AND s.connection_secret_ref IS NULL AND (${verifiedResourceCleanup('s', 'image_setup')})) IS NOT TRUE`,
|
||||
AND s.connection_secret_ref IS NULL AND (${verifiedResourceCleanup('s', 'image_setup')})) IS NOT TRUE`),
|
||||
// An unconfigured Slack draft has no provider identity or inbound runtime.
|
||||
// Setup still wakes through normal browser admission. Retain every enabled,
|
||||
// previously active, partially connected, foreign, or unsupported endpoint.
|
||||
`SELECT 1 FROM chat_endpoints endpoint WHERE (endpoint.provider = 'slack'
|
||||
workCheck("chat_endpoints", `SELECT 1 FROM chat_endpoints endpoint WHERE (endpoint.provider = 'slack'
|
||||
AND endpoint.status = 'draft' AND endpoint.setup->>'step' = 'provider_setup'
|
||||
AND (endpoint.setup - ARRAY['step', 'command']) = '{}'::jsonb
|
||||
AND (endpoint.setup->'command' IS NULL OR endpoint.setup->'command' = 'null'::jsonb
|
||||
@@ -129,33 +133,33 @@ const WORK_CHECKS = [
|
||||
AND EXISTS (SELECT 1 FROM tool_connections draft_connection
|
||||
WHERE draft_connection.id = endpoint.connection_id AND draft_connection.company_id = endpoint.company_id
|
||||
AND draft_connection.connection_purpose = 'channel' AND draft_connection.transport = 'chat_sdk'
|
||||
AND draft_connection.status = 'draft' AND draft_connection.enabled = false)) IS NOT TRUE`,
|
||||
`SELECT 1 FROM routines WHERE status NOT IN ('paused', 'archived')`,
|
||||
`SELECT 1 FROM routine_runs WHERE status NOT IN ('completed', 'failed', 'skipped', 'cancelled')`,
|
||||
`SELECT 1 FROM plugin_jobs WHERE status <> 'paused'`,
|
||||
`SELECT 1 FROM plugin_job_runs WHERE status NOT IN ('succeeded', 'failed', 'cancelled')`,
|
||||
`SELECT 1 FROM plugin_webhook_deliveries WHERE status NOT IN ('succeeded', 'failed')`,
|
||||
AND draft_connection.status = 'draft' AND draft_connection.enabled = false)) IS NOT TRUE`),
|
||||
workCheck("routines", `SELECT 1 FROM routines WHERE status NOT IN ('paused', 'archived')`),
|
||||
workCheck("routine_runs", `SELECT 1 FROM routine_runs WHERE status NOT IN ('completed', 'failed', 'skipped', 'cancelled')`),
|
||||
workCheck("plugin_jobs", `SELECT 1 FROM plugin_jobs WHERE status <> 'paused'`),
|
||||
workCheck("plugin_job_runs", `SELECT 1 FROM plugin_job_runs WHERE status NOT IN ('succeeded', 'failed', 'cancelled')`),
|
||||
workCheck("plugin_webhook_deliveries", `SELECT 1 FROM plugin_webhook_deliveries WHERE status NOT IN ('succeeded', 'failed')`),
|
||||
// Saved outbound MCP configuration and disabled drafts have no independent
|
||||
// listener. Keep local processes, channels and GitHub webhook bindings awake;
|
||||
// accepted calls, credential recovery and delivery work have separate fences.
|
||||
`SELECT 1 FROM tool_connections c WHERE c.status <> 'archived' AND (
|
||||
workCheck("tool_connections", `SELECT 1 FROM tool_connections c WHERE c.status <> 'archived' AND (
|
||||
(c.status = 'draft' AND c.enabled = false)
|
||||
OR (c.status = 'active' AND c.connection_purpose = 'ai' AND c.transport = 'runtime_auth')
|
||||
OR (c.status = 'active' AND c.connection_purpose = 'tool' AND c.transport = 'mcp_remote'
|
||||
AND NOT EXISTS (SELECT 1 FROM connection_grants g WHERE g.connection_id = c.id
|
||||
AND (g.company_id <> c.company_id OR g.provider_tenant->'github' IS NOT NULL)))
|
||||
) IS NOT TRUE`,
|
||||
) IS NOT TRUE`),
|
||||
// An explicit board escalation has no automatic retry. Retain the visible
|
||||
// action and all execution/cleanup checks while the board decides what to do.
|
||||
`SELECT 1 FROM issue_recovery_actions a WHERE a.status NOT IN ('resolved', 'cancelled')
|
||||
AND (${passiveBoardRecovery('a')}) IS NOT TRUE`,
|
||||
`SELECT 1 FROM status_cards WHERE archived_at IS NULL`,
|
||||
`SELECT 1 FROM external_objects WHERE NOT is_terminal OR next_refresh_at IS NOT NULL`,
|
||||
workCheck("issue_recovery_actions", `SELECT 1 FROM issue_recovery_actions a WHERE a.status NOT IN ('resolved', 'cancelled')
|
||||
AND (${passiveBoardRecovery('a')}) IS NOT TRUE`),
|
||||
workCheck("status_cards", `SELECT 1 FROM status_cards WHERE archived_at IS NULL`),
|
||||
workCheck("external_objects", `SELECT 1 FROM external_objects WHERE NOT is_terminal OR next_refresh_at IS NOT NULL`),
|
||||
// Login history keeps its provider reference after teardown. A terminal
|
||||
// label alone is insufficient: require a finished, unclaimed session and
|
||||
// positive cleanup evidence for its resource in the same company/environment.
|
||||
// Cast only UUID-shaped session references, leaving both lease indexes usable.
|
||||
`SELECT 1 FROM adapter_auth_sessions s WHERE
|
||||
workCheck("adapter_auth_sessions", `SELECT 1 FROM adapter_auth_sessions s WHERE
|
||||
s.status NOT IN ('authenticated', 'completed', 'failed', 'timed_out', 'cancelled')
|
||||
OR s.finished_at IS NULL OR s.promotion_expires_at IS NOT NULL
|
||||
OR (s.provider_lease_id IS NOT NULL AND NOT EXISTS (
|
||||
@@ -174,11 +178,11 @@ const WORK_CHECKS = [
|
||||
WHEN s.provider_lease_id ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
||||
THEN s.provider_lease_id::uuid END)
|
||||
AND ((l.status IN ('released', 'expired') OR (${finishedFailedLease("l")})) AND l.cleanup_status = 'success'
|
||||
AND l.released_at IS NOT NULL) IS NOT TRUE))`,
|
||||
AND l.released_at IS NOT NULL) IS NOT TRUE))`),
|
||||
// Answer delivery history is not work once its acknowledged handoff has
|
||||
// finished. Keep pending deliveries, unfinished target runs, and missing or
|
||||
// unfinished fallback wake receipts awake. Other work checks still apply.
|
||||
`SELECT 1 FROM issue_question_response_deliveries d WHERE
|
||||
workCheck("issue_question_response_deliveries", `SELECT 1 FROM issue_question_response_deliveries d WHERE
|
||||
d.status NOT IN ('delivered', 'fallback_queued', 'failed')
|
||||
OR (d.status IN ('delivered', 'fallback_queued') AND d.acknowledged_at IS NULL)
|
||||
OR (d.status = 'delivered' AND d.target_run_id IS NULL)
|
||||
@@ -195,36 +199,36 @@ const WORK_CHECKS = [
|
||||
OR (w.status = 'coalesced' AND EXISTS (
|
||||
SELECT 1 FROM heartbeat_runs r WHERE r.id = w.run_id
|
||||
AND r.company_id = w.company_id AND r.agent_id = w.agent_id
|
||||
AND ${finishedRun("r")})))))`,
|
||||
AND ${finishedRun("r")})))))`),
|
||||
// Cloud owns inbound event durability. A processed receipt is idempotency
|
||||
// history; any downstream run, wake or delivery is still checked separately.
|
||||
`SELECT 1 FROM connection_event_deliveries WHERE status <> 'processed' OR processed_at IS NULL`,
|
||||
workCheck("connection_event_deliveries", `SELECT 1 FROM connection_event_deliveries WHERE status <> 'processed' OR processed_at IS NULL`),
|
||||
// A delivered outbox row is history after its exact accepted wake finishes.
|
||||
// A system-retired connection card may also be parked behind a still-pending
|
||||
// response. Keep the outbox intact: the normal reply path resumes delivery.
|
||||
`SELECT 1 FROM connection_intent_deliveries d WHERE
|
||||
workCheck("connection_intent_deliveries", `SELECT 1 FROM connection_intent_deliveries d WHERE
|
||||
(d.delivered_at IS NULL AND (${connectionDeliveryWaitingForResponse('d')}) IS NOT TRUE)
|
||||
OR EXISTS (SELECT 1 FROM agent_wakeup_requests connection_wake
|
||||
WHERE connection_wake.company_id = d.company_id AND connection_wake.idempotency_key IN (
|
||||
'connection-intent:' || d.interaction_id::text || ':accepted',
|
||||
'connection-intent:' || d.interaction_id::text || ':rejected',
|
||||
'connection-intent:' || d.interaction_id::text || ':expired')
|
||||
AND (${finishedIdleWake('connection_wake')}) IS NOT TRUE)`,
|
||||
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`,
|
||||
workCheck("feedback_exports", `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`),
|
||||
// Skill package conversion and retention are optional maintenance. Legacy
|
||||
// contents remain authoritative until verified conversion; retries may wait
|
||||
// for ingress, including when external package storage is disabled.
|
||||
`SELECT 1 FROM project_skill_sources b JOIN companies c ON c.id = b.company_id
|
||||
WHERE c.status = 'active' AND b.active AND b.source_id IS NULL`,
|
||||
`SELECT 1 FROM company_skill_sources s JOIN companies c ON c.id = s.company_id
|
||||
workCheck("project_skill_sources", `SELECT 1 FROM project_skill_sources b JOIN companies c ON c.id = b.company_id
|
||||
WHERE c.status = 'active' AND b.active AND b.source_id IS NULL`),
|
||||
workCheck("company_skill_sources", `SELECT 1 FROM company_skill_sources s JOIN companies c ON c.id = s.company_id
|
||||
WHERE c.status = 'active' AND s.enabled AND s.automatic_updates
|
||||
AND (s.access_scope = 'company' OR EXISTS (
|
||||
SELECT 1 FROM project_skill_sources b WHERE b.source_id = s.id AND b.company_id = s.company_id AND b.active))`,
|
||||
`SELECT 1 FROM native_run_finalizations f WHERE (
|
||||
SELECT 1 FROM project_skill_sources b WHERE b.source_id = s.id AND b.company_id = s.company_id AND b.active))`),
|
||||
workCheck("native_run_finalizations", `SELECT 1 FROM native_run_finalizations f WHERE (
|
||||
f.phase = 'committed' AND f.lease_owner IS NULL AND f.lease_expires_at IS NULL
|
||||
AND f.next_attempt_at IS NULL AND f.control_deadline_at IS NULL
|
||||
AND f.recovery_state IS NULL AND f.recovery_request_id IS NULL
|
||||
@@ -258,7 +262,7 @@ const WORK_CHECKS = [
|
||||
AND (effect.company_id <> f.company_id OR effect.issue_id <> f.issue_id
|
||||
OR effect.delivery_state <> 'delivered' OR effect.delivered_at IS NULL
|
||||
OR effect.next_attempt_at IS NOT NULL))
|
||||
) IS NOT TRUE`,
|
||||
) 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.
|
||||
...[
|
||||
@@ -275,7 +279,7 @@ const WORK_CHECKS = [
|
||||
"dot_runner_assignments", "dot_runner_operations", "dot_mailbox_items",
|
||||
"muse_agent_bindings", "muse_credentials", "muse_runner_assignments",
|
||||
"muse_runner_operations", "muse_mailbox_items", "muse_input_deliveries", "external_agent_holds",
|
||||
].map((table) => `SELECT 1 FROM ${table}`),
|
||||
].map((table) => workCheck(table, `SELECT 1 FROM ${table}`)),
|
||||
] as const;
|
||||
|
||||
function sameQuietHold(before: IdleSleepDrainStatus, after: IdleSleepDrainStatus, now: number): boolean {
|
||||
@@ -288,94 +292,166 @@ function sameQuietHold(before: IdleSleepDrainStatus, after: IdleSleepDrainStatus
|
||||
/** Inventory persisted work after the task-drain admission hold lands.
|
||||
* Only a caller holding the exact idle owner may receive an empty report.
|
||||
*/
|
||||
export interface IdleSleepSafetyOptions {
|
||||
collectDiagnostics?: true;
|
||||
}
|
||||
|
||||
export async function readIdleSleepSafety(
|
||||
db: Db,
|
||||
getDrainStatus: () => IdleSleepDrainStatus,
|
||||
now: () => number = Date.now,
|
||||
ownerId?: string,
|
||||
inspectLocalWork: () => Promise<IdleLocalWork> = readIdleLocalWork,
|
||||
inspectLocalWork: (diagnostic?: IdleSleepDiagnosticSink) => Promise<IdleLocalWork> =
|
||||
diagnostic => readIdleLocalWork({ diagnostic }),
|
||||
inspectPlugins?: InspectIdlePlugins,
|
||||
prepareBackup?: () => Promise<boolean>,
|
||||
scheduledRoutines = false,
|
||||
scheduledIssueMonitors = false,
|
||||
options: IdleSleepSafetyOptions = {},
|
||||
): Promise<IdleSleepSafety> {
|
||||
const unknown: IdleSleepSafety = { version: 1, backgroundWork: "unknown" };
|
||||
const diagnostic = options.collectDiagnostics ? createIdleSleepDiagnostics() : undefined;
|
||||
const report = (backgroundWork: IdleLocalWork, scheduledWake?: ScheduledWake): IdleSleepSafety => {
|
||||
const diagnostics = diagnostic?.report();
|
||||
if (backgroundWork === "none" && diagnostics?.checks.some(check =>
|
||||
check.state === "unknown" || check.state === "not_checked")) backgroundWork = "unknown";
|
||||
return { version: backgroundWork === "none" && scheduledWake ? 2 : 1, backgroundWork,
|
||||
...(backgroundWork === "none" && scheduledWake ? { scheduledWake } : {}),
|
||||
...(diagnostics ? { diagnostics } : {}) };
|
||||
};
|
||||
const before = getDrainStatus();
|
||||
const localBefore = idleWorkSnapshot();
|
||||
if (!sameQuietHold(before, before, now())) return unknown;
|
||||
const owned = () => Boolean(ownerId && ownerId === before.ownerId && before.expiresAt !== null);
|
||||
if (!sameQuietHold(before, before, now()) || (diagnostic && !owned())) {
|
||||
diagnostic?.record("inspection.hold", "unknown");
|
||||
diagnostic?.record("durable.snapshot", "not_checked");
|
||||
return report("unknown");
|
||||
}
|
||||
diagnostic?.record("inspection.hold", "none");
|
||||
let failureReason = "local.inspection";
|
||||
try {
|
||||
// Reject known local blockers before querying (and potentially waking)
|
||||
// the tenant database. Keep the final inspection below as a second fence.
|
||||
const localFirst = await inspectLocalWork();
|
||||
if (localFirst !== "none") return { version: 1, backgroundWork: localFirst };
|
||||
const localFirst = await inspectLocalWork(diagnostic?.record);
|
||||
diagnostic?.record("local.inspection", localFirst);
|
||||
if (localFirst !== "none" && !diagnostic) return report(localFirst);
|
||||
let quietPlugins: string[] = [];
|
||||
let pluginState: IdleLocalWork = "none";
|
||||
if (inspectPlugins) {
|
||||
if (!ownerId || ownerId !== before.ownerId || before.expiresAt === null || localBefore.active !== 0) return unknown;
|
||||
const pluginWork = await inspectPlugins({ ownerId, expiresAt: before.expiresAt.getTime() });
|
||||
if (pluginWork.backgroundWork !== "none") return { version: 1, backgroundWork: pluginWork.backgroundWork };
|
||||
quietPlugins = pluginWork.pluginIds;
|
||||
const currentLocal = diagnostic ? idleWorkSnapshot() : localBefore;
|
||||
if (!owned() || currentLocal.active !== 0 || (diagnostic &&
|
||||
(!sameQuietHold(before, getDrainStatus(), now()) || currentLocal.generation !== localBefore.generation))) {
|
||||
if (!diagnostic) return report("unknown");
|
||||
diagnostic.record("plugins.runtime_drain", "not_checked");
|
||||
pluginState = "unknown";
|
||||
} else {
|
||||
failureReason = "plugins.runtime_drain";
|
||||
const pluginWork = await inspectPlugins({ ownerId: ownerId!, expiresAt: before.expiresAt!.getTime() });
|
||||
pluginState = pluginWork.backgroundWork;
|
||||
diagnostic?.record("plugins.runtime_drain", pluginState);
|
||||
if (pluginState !== "none" && !diagnostic) return report(pluginState);
|
||||
if (pluginState === "none") quietPlugins = pluginWork.pluginIds;
|
||||
}
|
||||
}
|
||||
if (diagnostic && !sameQuietHold(before, getDrainStatus(), now())) {
|
||||
diagnostic.record("inspection.hold", "unknown");
|
||||
diagnostic.record("durable.snapshot", "not_checked");
|
||||
return report("unknown");
|
||||
}
|
||||
failureReason = "durable.snapshot";
|
||||
let scheduledWake: ScheduledWake | undefined;
|
||||
const blocked = await db.transaction(async (tx) => {
|
||||
await tx.execute(sql`SET LOCAL statement_timeout = '5s'`);
|
||||
const checks = WORK_CHECKS.filter(query =>
|
||||
(!scheduledRoutines || query !== `SELECT 1 FROM routines WHERE status NOT IN ('paused', 'archived')`)
|
||||
&& (!scheduledIssueMonitors || query !== `SELECT 1 FROM issues WHERE status NOT IN ('done', 'cancelled')`))
|
||||
.map((query) => sql`EXISTS (${sql.raw(query)})`);
|
||||
checks.push(sql`EXISTS (SELECT 1 FROM agents JOIN companies ON companies.id = agents.company_id
|
||||
WHERE ${companyHoldMismatch()})`);
|
||||
checks.push(sql`EXISTS (SELECT 1 FROM heartbeat_runs WHERE ${pendingBudgetRunCancellationPredicate()})`);
|
||||
checks.push(sql`EXISTS (${nativeResponsePresentationPendingQuery()})`);
|
||||
const checks = WORK_CHECKS.filter(check =>
|
||||
(!scheduledRoutines || check.reason !== "routines")
|
||||
&& (!scheduledIssueMonitors || check.reason !== "issues"));
|
||||
checks.push(workCheck("agents.company_hold", sql`SELECT 1 FROM agents JOIN companies ON companies.id = agents.company_id
|
||||
WHERE ${companyHoldMismatch()}`));
|
||||
checks.push(workCheck("heartbeat_runs.budget_cancellation", sql`SELECT 1 FROM heartbeat_runs WHERE ${pendingBudgetRunCancellationPredicate()}`));
|
||||
checks.push(workCheck("native_response_presentation", nativeResponsePresentationPendingQuery()));
|
||||
// Only the current worker's owned runtime drain can exempt a plugin.
|
||||
// Missing workers, old SDKs, and newly installed plugins still block.
|
||||
checks.push(quietPlugins.length === 0
|
||||
? sql`EXISTS (SELECT 1 FROM plugins WHERE status <> 'disabled')`
|
||||
: sql`EXISTS (SELECT 1 FROM plugins WHERE status <> 'disabled' AND id NOT IN (${sql.join(quietPlugins.map((id) => sql`${id}::uuid`), sql`, `)}))`);
|
||||
const rows = await tx.execute<{ blocked: boolean }>(sql`SELECT ${sql.join(checks, sql` OR `)} AS blocked`);
|
||||
if (rows.length !== 1 || typeof rows[0]?.blocked !== "boolean") return undefined;
|
||||
if (rows[0].blocked) return true;
|
||||
checks.push(workCheck("plugins", quietPlugins.length === 0
|
||||
? sql`SELECT 1 FROM plugins WHERE status <> 'disabled'`
|
||||
: sql`SELECT 1 FROM plugins WHERE status <> 'disabled' AND id NOT IN (${sql.join(quietPlugins.map((id) => sql`${id}::uuid`), sql`, `)})`));
|
||||
let found: boolean;
|
||||
if (diagnostic) {
|
||||
diagnostic.expect(checks.map(check => check.reason));
|
||||
const rows = await tx.execute<Record<string, boolean>>(sql`SELECT ${sql.join(checks.map(check =>
|
||||
sql`EXISTS (${check.query}) AS ${sql.identifier(check.reason)}`), sql`, `)}`);
|
||||
if (rows.length !== 1 || Object.keys(rows[0] ?? {}).length !== checks.length
|
||||
|| checks.some(check => !Object.hasOwn(rows[0]!, check.reason) || typeof rows[0]![check.reason] !== "boolean")) return undefined;
|
||||
for (const check of checks) diagnostic.record(check.reason, rows[0]![check.reason] ? "present" : "none");
|
||||
diagnostic.record("durable.snapshot", "none");
|
||||
found = checks.some(check => rows[0]![check.reason]);
|
||||
} else {
|
||||
const rows = await tx.execute<{ blocked: boolean }>(sql`SELECT ${sql.join(checks.map(check => sql`EXISTS (${check.query})`), sql` OR `)} AS blocked`);
|
||||
if (rows.length !== 1 || typeof rows[0]?.blocked !== "boolean") return undefined;
|
||||
found = rows[0].blocked;
|
||||
}
|
||||
if (found && !diagnostic) return true;
|
||||
if (scheduledRoutines) {
|
||||
if (!ownerId || ownerId !== before.ownerId) return undefined;
|
||||
const schedule = await readIdleRoutineSchedule(tx, ownerId, now());
|
||||
if (schedule.blocked) return true;
|
||||
if (!owned()) return undefined;
|
||||
const schedule = await readIdleRoutineSchedule(tx, ownerId!, now());
|
||||
diagnostic?.record("routines.schedule", schedule.blocked ? "present" : "none");
|
||||
if (schedule.blocked && !diagnostic) return true;
|
||||
found ||= schedule.blocked;
|
||||
scheduledWake = schedule.scheduledWake;
|
||||
}
|
||||
if (scheduledIssueMonitors) {
|
||||
if (!scheduledRoutines || !ownerId || ownerId !== before.ownerId) return undefined;
|
||||
const schedule = await readIdleIssueSchedule(tx, ownerId, now());
|
||||
if (schedule.blocked) return true;
|
||||
if (!scheduledRoutines || !owned()) return undefined;
|
||||
const schedule = await readIdleIssueSchedule(tx, ownerId!, now());
|
||||
diagnostic?.record("issues.schedule", schedule.blocked ? "present" : "none");
|
||||
if (schedule.blocked && !diagnostic) return true;
|
||||
found ||= schedule.blocked;
|
||||
scheduledWake = combineIdleSchedules(scheduledWake, schedule.scheduledWake, schedule.generation);
|
||||
}
|
||||
return false;
|
||||
return found;
|
||||
}, { isolationLevel: "repeatable read", accessMode: "read only" });
|
||||
if (blocked === undefined || !sameQuietHold(before, getDrainStatus(), now())) return unknown;
|
||||
if (blocked) return { version: 1, backgroundWork: "present" };
|
||||
if (!ownerId || ownerId !== before.ownerId || before.expiresAt === null || localBefore.active !== 0) return unknown;
|
||||
if (blocked === undefined) {
|
||||
diagnostic?.record("durable.snapshot", "unknown");
|
||||
return report("unknown");
|
||||
}
|
||||
if (!sameQuietHold(before, getDrainStatus(), now())) {
|
||||
diagnostic?.record("inspection.hold", "unknown");
|
||||
return report("unknown");
|
||||
}
|
||||
if (blocked && !diagnostic) return report("present");
|
||||
if (!owned() || (!diagnostic && localBefore.active !== 0)) return report("unknown");
|
||||
let expectedGeneration = localBefore.generation;
|
||||
if (prepareBackup) {
|
||||
if (!sameQuietHold(before, getDrainStatus(), now()) ||
|
||||
idleWorkSnapshot().generation !== expectedGeneration || idleWorkSnapshot().active !== 0) return unknown;
|
||||
// The runner owns exactly one tracked-work receipt through dump,
|
||||
// archive verification and fsync. Its start/finish are the only work
|
||||
// allowed during this scan. Any concurrent work invalidates the dump.
|
||||
if (!(await prepareBackup())) return unknown;
|
||||
expectedGeneration += 2;
|
||||
if (diagnostic) {
|
||||
diagnostic.record("backup.checkpoint", "not_checked");
|
||||
} else {
|
||||
if (!sameQuietHold(before, getDrainStatus(), now()) ||
|
||||
idleWorkSnapshot().generation !== expectedGeneration || idleWorkSnapshot().active !== 0) return report("unknown");
|
||||
// The runner owns exactly one tracked-work receipt through dump,
|
||||
// archive verification and fsync. Its start/finish are the only work
|
||||
// allowed during this scan. Any concurrent work invalidates the dump.
|
||||
if (!(await prepareBackup())) return report("unknown");
|
||||
expectedGeneration += 2;
|
||||
}
|
||||
}
|
||||
const local = await inspectLocalWork();
|
||||
failureReason = "local.inspection";
|
||||
const local = await inspectLocalWork(diagnostic?.record);
|
||||
diagnostic?.record("local.inspection", local);
|
||||
const after = getDrainStatus();
|
||||
const localAfter = idleWorkSnapshot();
|
||||
if (!sameQuietHold(before, after, now()) || localAfter.active !== 0 ||
|
||||
expectedGeneration !== localAfter.generation) return unknown;
|
||||
if (local === "none" && scheduledWake) {
|
||||
if (Date.parse(scheduledWake.nextWakeAt) <= now()) return { version: 1, backgroundWork: "present" };
|
||||
return { version: 2, backgroundWork: "none", scheduledWake };
|
||||
expectedGeneration !== localAfter.generation) {
|
||||
diagnostic?.record("inspection.generation", "unknown");
|
||||
return report("unknown");
|
||||
}
|
||||
return { version: 1, backgroundWork: local };
|
||||
|
||||
diagnostic?.record("inspection.generation", "none");
|
||||
if (diagnostic && (localFirst === "unknown" || local === "unknown" || pluginState === "unknown")) return report("unknown");
|
||||
if (diagnostic && (blocked || localFirst === "present" || pluginState === "present")) return report("present");
|
||||
if (local === "none" && scheduledWake && Date.parse(scheduledWake.nextWakeAt) <= now()) {
|
||||
diagnostic?.record("inspection.hold", "unknown");
|
||||
return report("present");
|
||||
}
|
||||
return report(local, scheduledWake);
|
||||
} catch {
|
||||
// Missing migrations, unknown state, malformed configuration, timeouts and
|
||||
// database failures never authorize stopping the application. Do not leak
|
||||
// SQL, configuration or tenant data through this control response.
|
||||
return unknown;
|
||||
diagnostic?.record(failureReason, "unknown");
|
||||
return report("unknown");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user