From 039a74c582cc23211874e8bb8fa378fc2d31c1bd Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 21 Sep 2026 09:39:45 -0500 Subject: [PATCH] fix: keep readiness confirmation responsive near its deadline Co-Authored-By: Paperclip --- .../src/live/warm-attachment-readiness.test.ts | 18 +++++++++++++++++- .../src/live/warm-attachment-readiness.ts | 11 +++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/paperclip-runner/src/live/warm-attachment-readiness.test.ts b/packages/paperclip-runner/src/live/warm-attachment-readiness.test.ts index 99707149a0a..e6ecd484c35 100644 --- a/packages/paperclip-runner/src/live/warm-attachment-readiness.test.ts +++ b/packages/paperclip-runner/src/live/warm-attachment-readiness.test.ts @@ -46,5 +46,21 @@ it("does not reset the probe budget when readiness alternates", async () => { .then(() => "unexpected-ready", error => String(error)); await vi.advanceTimersByTimeAsync(122_000); expect(await result).toContain("native_runner_warm_attachment_not_quiescent"); - expect(calls).toBeLessThan(250); + expect(calls).toBeLessThan(350); +}); + +it("confirms readiness near the deadline after one unsettled observation", async () => { + vi.useFakeTimers(); + vi.setSystemTime(0); + let recoveredProbes = 0; + const result = waitForWarmAttachmentReadiness({ + graceMs: 5_000, + waitForConnection: async () => {}, + snapshot: async () => ({ + warmAttachReady: Date.now() >= 4_400 && ++recoveredProbes !== 2, + warmAttachBlockers: [], + }), + }).then(() => "ready", error => String(error)); + await vi.advanceTimersByTimeAsync(5_000); + expect(await result).toBe("ready"); }); diff --git a/packages/paperclip-runner/src/live/warm-attachment-readiness.ts b/packages/paperclip-runner/src/live/warm-attachment-readiness.ts index d45c7be8ba4..70057c776a4 100644 --- a/packages/paperclip-runner/src/live/warm-attachment-readiness.ts +++ b/packages/paperclip-runner/src/live/warm-attachment-readiness.ts @@ -8,13 +8,17 @@ export async function waitForWarmAttachmentReadiness(input: { const deadline = Date.now() + input.graceMs; let consecutiveReadyProbes = 0; let lastBlockers: unknown = null; - let blockedDelayMs = 25; + let probes = 0; // Each probe is a durable command. A 25 ms loop over the remote 120 second // reconnect budget can exhaust its 500-command journal before that budget. - const maxBlockedDelayMs = Math.max(1_000, Math.ceil(input.graceMs / 100)); + // Leave room for both ready-confirmation probes and ordinary commands. The + // interval is proportional to the deadline, so a short local deadline never + // inherits a one-second delay after a transient readiness regression. + const blockedDelayMs = Math.max(25, Math.ceil(input.graceMs / 160)); while (Date.now() < deadline) { await input.waitForConnection(deadline); const snapshot = await input.snapshot(deadline); + probes += 1; if (snapshot.warmAttachReady !== true && JSON.stringify(snapshot.warmAttachBlockers) !== JSON.stringify(lastBlockers)) { input.onBlocked?.(snapshot.warmAttachBlockers); } @@ -25,8 +29,7 @@ export async function waitForWarmAttachmentReadiness(input: { } else { consecutiveReadyProbes = 0; } - const delayMs = snapshot.warmAttachReady === true ? 25 : blockedDelayMs; - if (snapshot.warmAttachReady !== true) blockedDelayMs = Math.min(blockedDelayMs * 2, maxBlockedDelayMs); + const delayMs = snapshot.warmAttachReady === true || probes < 8 ? 25 : blockedDelayMs; await new Promise(resolve => setTimeout(resolve, Math.min(delayMs, Math.max(0, deadline - Date.now())))); } throw new Error(`native_runner_warm_attachment_not_quiescent: ${JSON.stringify(lastBlockers)}`);