mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-08 22:13:59 +01:00
fix(server): prove completed history before idle sleep (#18646)
Reviewed and verified at 592bce69c6.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
367f29478d
commit
ccc63ea458
44 files changed
+4100
-112
No files matched your search
@@ -46,6 +46,7 @@ import {
|
||||
import { and, desc, eq, gte, inArray, isNull, lte, ne, or, sql } from "drizzle-orm";
|
||||
import { asNumber, asString, parseObject, renderTemplate } from "../adapters/utils.js";
|
||||
import { conflict, unprocessable } from "../errors.js";
|
||||
import { registerIdleLocalWorkProbe, type IdleLocalWork } from "./idle-local-work.js";
|
||||
import { withExecutionWorkspaceLifecycleLock } from "./execution-workspace-lifecycle-lock.js";
|
||||
import { resolveHomeAwarePath } from "../home-paths.js";
|
||||
import { hasVerifiedWorktreeSeedManifest, isVerifiedWorktreeSeedManifest } from "../worktree-seed-manifest.js";
|
||||
@@ -283,6 +284,23 @@ const quarantinedRuntimeExposurePorts = new Set<number>();
|
||||
* companion together or not at all.
|
||||
*/
|
||||
const exposurePortPairClaims = new ExposurePortPairClaims();
|
||||
let runtimeReconstructionComplete = false;
|
||||
let runtimeStopsInFlight = 0;
|
||||
let runtimeReconstructionEpoch = 0;
|
||||
let runtimeReconciliation: ReturnType<typeof reconstructRuntimeServices> | null = null;
|
||||
|
||||
/** Read owner memory only. Durable stopped rows do not describe a surviving
|
||||
* registry process, an allocation being published, or unfinished exposure cleanup. */
|
||||
export function readWorkspaceRuntimeIdleWork(): IdleLocalWork {
|
||||
if (!runtimeReconstructionComplete || runtimeReconciliation !== null) return "unknown";
|
||||
return runtimeStopsInFlight || runtimeServicesById.size || runtimeProvisionByWorkspace.size || runtimeControlStartByOwner.size
|
||||
|| runtimeReplacementClaimsByReuseKey.size || quarantinedRuntimeExposurePorts.size
|
||||
|| exposurePortPairClaims.activePorts().size
|
||||
|| [...inFlightAllocatedPorts.values()].some(until => until > Date.now()) ? "present" : "none";
|
||||
}
|
||||
// This registry and its probe share the application process lifetime, like the
|
||||
// native-session owner. Stopping one resource must not unregister its siblings.
|
||||
registerIdleLocalWorkProbe(readWorkspaceRuntimeIdleWork);
|
||||
/**
|
||||
* Execution-workspace statuses that still hold an exclusive lease. `archived`
|
||||
* is the only terminal state; everything else — including `idle` — is a lane an
|
||||
@@ -525,6 +543,8 @@ type ProcessOutputAccumulator = {
|
||||
export async function resetRuntimeServicesForTests(
|
||||
opts: { terminateProcesses?: boolean; simulateSupervisorExit?: boolean } = {},
|
||||
) {
|
||||
runtimeReconstructionComplete = false;
|
||||
runtimeReconstructionEpoch += 1;
|
||||
if (opts.terminateProcesses) {
|
||||
for (const serviceId of [...runtimeServicesById.keys()]) {
|
||||
await stopRuntimeService(serviceId).catch(() => undefined);
|
||||
@@ -7155,7 +7175,21 @@ async function cleanupRecordExposure(
|
||||
await persistRuntimeServiceRecord(record.db, record).catch(() => undefined);
|
||||
}
|
||||
|
||||
async function withRuntimeStopWork<T>(stop: () => Promise<T>): Promise<T> {
|
||||
runtimeStopsInFlight += 1;
|
||||
try { return await stop(); }
|
||||
catch (error) {
|
||||
runtimeReconstructionComplete = false;
|
||||
runtimeReconstructionEpoch += 1;
|
||||
throw error;
|
||||
} finally { runtimeStopsInFlight -= 1; }
|
||||
}
|
||||
|
||||
async function stopRuntimeService(serviceId: string) {
|
||||
return withRuntimeStopWork(() => stopRuntimeServiceRecord(serviceId));
|
||||
}
|
||||
|
||||
async function stopRuntimeServiceRecord(serviceId: string) {
|
||||
const record = runtimeServicesById.get(serviceId);
|
||||
if (!record) return;
|
||||
clearIdleTimer(record);
|
||||
@@ -8296,7 +8330,11 @@ export async function releaseRuntimeServicesForRun(runId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function stopRuntimeServicesForExecutionWorkspace(input: {
|
||||
export async function stopRuntimeServicesForExecutionWorkspace(input: Parameters<typeof stopRuntimeServicesForExecutionWorkspaceOwned>[0]) {
|
||||
return withRuntimeStopWork(() => stopRuntimeServicesForExecutionWorkspaceOwned(input));
|
||||
}
|
||||
|
||||
async function stopRuntimeServicesForExecutionWorkspaceOwned(input: {
|
||||
db?: Db;
|
||||
executionWorkspaceId: string;
|
||||
workspaceCwd?: string | null;
|
||||
@@ -8351,7 +8389,11 @@ export async function stopRuntimeServicesForExecutionWorkspace(input: {
|
||||
}
|
||||
}
|
||||
|
||||
export async function stopRuntimeServicesForProjectWorkspace(input: {
|
||||
export async function stopRuntimeServicesForProjectWorkspace(input: Parameters<typeof stopRuntimeServicesForProjectWorkspaceOwned>[0]) {
|
||||
return withRuntimeStopWork(() => stopRuntimeServicesForProjectWorkspaceOwned(input));
|
||||
}
|
||||
|
||||
async function stopRuntimeServicesForProjectWorkspaceOwned(input: {
|
||||
db?: Db;
|
||||
projectWorkspaceId: string;
|
||||
runtimeServiceId?: string | null;
|
||||
@@ -8577,6 +8619,19 @@ export async function refreshPersistedRuntimeServiceHealth(input: {
|
||||
}
|
||||
|
||||
export async function reconcilePersistedRuntimeServicesOnStartup(db: Db) {
|
||||
if (runtimeReconciliation) return runtimeReconciliation;
|
||||
runtimeReconstructionComplete = false;
|
||||
const epoch = ++runtimeReconstructionEpoch;
|
||||
runtimeReconciliation = reconstructRuntimeServices(db).then(result => {
|
||||
if (epoch === runtimeReconstructionEpoch) {
|
||||
runtimeReconstructionComplete = result.restartFailed === 0 && result.exposureReservationDrift.length === 0;
|
||||
}
|
||||
return result;
|
||||
}).finally(() => { runtimeReconciliation = null; });
|
||||
return runtimeReconciliation;
|
||||
}
|
||||
|
||||
async function reconstructRuntimeServices(db: Db) {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(workspaceRuntimeServices)
|
||||
|
||||
Reference in new issue
Block a user