Files
PaperClipAI/server/src/services/idle-local-work.ts
T
DottaandPaperclip 7338fdaeae Merge reviewed sleep blocker inventory into ingress work
Preserve the shared diagnostics and select ingress predicates by stable reason identity. Keep all legacy blockers conservative without verified incoming wake coverage.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-02 11:41:32 -06:00

113 lines
5.3 KiB
TypeScript

import { idleApiActivity } from "./idle-api-activity.js";
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>();
const localWorkProbes = new Set<() => IdleLocalWork>();
export function registerIdleLocalWorkProbe(probe: () => IdleLocalWork): () => void {
localWorkProbes.add(probe);
return () => { localWorkProbes.delete(probe); };
}
export function registerIdleSpoolDirectory(directory: string): void { spoolDirectories.add(path.resolve(directory)); }
export const idleAccountingSpoolPath = () => path.join(resolvePaperclipInstanceRoot(), "accounting-receipts");
export const idleOrphanSpoolPath = () => process.env.SANDBOX_ORPHAN_CLEANUP_SPOOL_DIR ??
path.join(resolvePaperclipInstanceRoot(), "data", "sandbox-orphan-cleanup");
export const idleQuotaRecoveryPath = () => path.join(resolvePaperclipInstanceRoot(), "quota-credential-recovery");
let startupComplete = false;
let ingressTracked = false;
let scheduledBackups = false;
export function markIdleIngressTracked() { ingressTracked = true; }
export function markIdleStartupComplete(options: { scheduledBackups: boolean }) {
scheduledBackups = options.scheduledBackups;
startupComplete = true;
}
/** Any entry, including malformed JSON, a temp file or a failed probe, is
* evidence to retain. Recovery readers intentionally skip some such files;
* they cannot certify that stopping the process is safe. */
export async function inspectIdleSpool(directory: string): Promise<IdleLocalWork> {
try {
const info = await fs.lstat(directory);
if (!info.isDirectory() || info.isSymbolicLink()) return "unknown";
const dir = await fs.opendir(directory);
try { return (await dir.read()) === null ? "none" : "present"; }
finally { await dir.close(); }
} catch (error) {
return (error as NodeJS.ErrnoException).code === "ENOENT" ? "none" : "unknown";
}
}
/** Credential journals are grouped by company. Successful recovery leaves its
* empty company directory behind; every file, probe or unexpected entry still
* blocks. Never decrypt journals or follow directory symlinks during a scan. */
export async function inspectIdleQuotaRecovery(directory: string): Promise<IdleLocalWork> {
try {
const info = await fs.lstat(directory);
if (!info.isDirectory() || info.isSymbolicLink()) return "unknown";
const dir = await fs.opendir(directory);
try {
for (let entry = await dir.read(); entry !== null; entry = await dir.read()) {
if (!entry.isDirectory() || entry.isSymbolicLink()
|| !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(entry.name)) return "present";
const state = await inspectIdleSpool(path.join(directory, entry.name));
if (state !== "none") return state;
}
return "none";
} finally { await dir.close(); }
} catch (error) {
return (error as NodeJS.ErrnoException).code === "ENOENT" ? "none" : "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.
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 = [idleApiActivity.active() ? "present" : "none", ...[...localWorkProbes].map(probe => probe())];
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(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;
}),
]);
states.push(...results);
if (states.includes("unknown")) return "unknown";
return states.includes("present") ? "present" : "none";
}