From 40eb34a09e2e13f2f7535d3dd5b108053dc7bebd Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 30 Oct 2026 21:42:47 -0500 Subject: [PATCH] Separate controller lease evidence from workspace ownership Co-Authored-By: Paperclip --- server/src/__tests__/chat-channels.integration.test.ts | 2 +- server/src/__tests__/live-recovery-work.test.ts | 2 +- server/src/__tests__/slack-conversation-lifecycle.test.ts | 4 ++-- server/src/services/heartbeat/recovery.ts | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/server/src/__tests__/chat-channels.integration.test.ts b/server/src/__tests__/chat-channels.integration.test.ts index 8b10fe2eec7..dc432e14cc3 100644 --- a/server/src/__tests__/chat-channels.integration.test.ts +++ b/server/src/__tests__/chat-channels.integration.test.ts @@ -76890,7 +76890,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { resolveSessionBeforeForWakeup: async () => null, }); return adapter.withIssueExecutionLock({ companyId: context.fixture.companyId, runId: releaseRunId, now: new Date() }, async (_locked, ports) => { - const target = { companyId: context.fixture.companyId, wakeId: context.action.id, now: new Date() }; + const target = { companyId: context.fixture.companyId, wakeId: context.action.id, agentId: context.fixture.assignedAgentId, now: new Date() }; if (change === "queue rejection") await ports.transaction.cancelDeferredWake({ ...target, reason: "PRIVATE rejection" }); else await ports.transaction.failDeferredWake(target); return { outcome: { kind: "released" }, postCommitEffects: [] }; diff --git a/server/src/__tests__/live-recovery-work.test.ts b/server/src/__tests__/live-recovery-work.test.ts index 9560e303561..c8da4886c80 100644 --- a/server/src/__tests__/live-recovery-work.test.ts +++ b/server/src/__tests__/live-recovery-work.test.ts @@ -109,7 +109,7 @@ describe("residual recovery production matrix", () => { expect(await db.select().from(heartbeatRuns).where(sql`${heartbeatRuns.status} in ('queued', 'running')`)).toHaveLength(0); const quietAt = Date.now(); if (vi.isFakeTimers()) await advance(31_000); - else await vi.waitFor(() => expect(coordinator.nextWakeAt()).toBeNull(), { timeout: 35_000 }); + else { await realDelay(31_000); await Promise.allSettled([...passes]); } console.info("quiet-drain", JSON.stringify({ quietAt, now: Date.now(), active: idleWorkSnapshot().active, next: [...deadlines].map(([q, at]) => [q, at()]), passes: Object.fromEntries([...deadlines.keys()].map(queue => [queue, timeline.filter(event => Array.isArray(event) && event[0] === "pass" && event[1] === queue).length])), settled: timeline.filter(event => Array.isArray(event) && event[0] === "settled") })); await vi.waitFor(() => { expect(coordinator.nextWakeAt(), JSON.stringify([...deadlines].map(([name, at]) => [name, at()]))).toBeNull(); expect(idleWorkSnapshot().active).toBe(0); }); const queries: string[] = [], prior = db.$client.options.debug; diff --git a/server/src/__tests__/slack-conversation-lifecycle.test.ts b/server/src/__tests__/slack-conversation-lifecycle.test.ts index 2a1e7778416..dfb998e9065 100644 --- a/server/src/__tests__/slack-conversation-lifecycle.test.ts +++ b/server/src/__tests__/slack-conversation-lifecycle.test.ts @@ -451,8 +451,8 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu let entered = false; await adapter.withIssueExecutionLock({ companyId: f.companyId, runId: f.runId, now: new Date() }, async (_locked, ports) => { entered = true; - if (operation === "cancel_wake") await ports.transaction.cancelDeferredWake({ companyId: f.companyId, wakeId, reason: "cancelled", now: new Date() }); - if (operation === "fail_wake") await ports.transaction.failDeferredWake({ companyId: f.companyId, wakeId, now: new Date() }); + if (operation === "cancel_wake") await ports.transaction.cancelDeferredWake({ companyId: f.companyId, wakeId, agentId: f.agentId, reason: "cancelled", now: new Date() }); + if (operation === "fail_wake") await ports.transaction.failDeferredWake({ companyId: f.companyId, wakeId, agentId: f.agentId, now: new Date() }); return { outcome: { kind: "released" as const }, postCommitEffects: [] }; }); expect(entered).toBe(true); diff --git a/server/src/services/heartbeat/recovery.ts b/server/src/services/heartbeat/recovery.ts index 43aa81cffb9..76b77449d3f 100644 --- a/server/src/services/heartbeat/recovery.ts +++ b/server/src/services/heartbeat/recovery.ts @@ -2295,7 +2295,8 @@ export function createHeartbeatRecovery(db: Db, dependencies: HeartbeatRecoveryD runningProcesses.has(run.id) || activeRunExecutions.has(run.id) || coordinatorOwnedByCurrentController; - if (nativeRun && !locallyTracked && nativeControllerLeaseExpiresAt && nativeControllerLeaseExpiresAt > now) { + if (nativeRun && !locallyTracked && nativeControllerBootId && nativeControllerPid + && nativeControllerProcessStartedAt && nativeControllerLeaseExpiresAt && nativeControllerLeaseExpiresAt > now) { scope.onDeferredDeadline?.(nativeControllerLeaseExpiresAt.getTime()); continue; }