From 0b5a2c0411ed6bd982b5f87f9e6c1060a1d64a8a Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Fri, 10 Jul 2026 17:04:41 -0400 Subject: [PATCH] fix(worker): ensure reconciled boxes before acquire --- apps/agent-worker/src/user-container.ts | 16 +++++++++---- .../tests/unit/user-container.test.ts | 24 +++++++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/apps/agent-worker/src/user-container.ts b/apps/agent-worker/src/user-container.ts index 3b6890f..97c5763 100644 --- a/apps/agent-worker/src/user-container.ts +++ b/apps/agent-worker/src/user-container.ts @@ -13,6 +13,7 @@ export type BoxHandle = { boxName: string; release: () => void }; type Box = { name: string; + initialized: boolean; refs: Set; idleTimer?: ReturnType; }; @@ -71,21 +72,26 @@ export const acquireUserBox = (args: { }): Promise => withLock(args.username, async () => { let box = boxes.get(args.username); - let needsEnsure = false; if (box?.idleTimer) { clearTimeout(box.idleTimer); box.idleTimer = undefined; } if (!box) { - box = { name: userContainerName(args.username), refs: new Set() }; + box = { + name: userContainerName(args.username), + initialized: false, + refs: new Set(), + }; boxes.set(args.username, box); - needsEnsure = true; } // Register the ref before the slow Docker call so a failed acquire can // release it without leaking registry state. const handle = makeHandle(args.username, box); try { - if (needsEnsure) box.name = await ensureUserContainer(args); + if (!box.initialized) { + box.name = await ensureUserContainer(args); + box.initialized = true; + } return handle; } catch (error) { handle.release(); @@ -103,7 +109,7 @@ export const reconcileExistingBoxes = async (): Promise => { for (const name of names) { const username = name.replace(/^spoon-box-/, ''); if (boxes.has(username)) continue; - const box: Box = { name, refs: new Set() }; + const box: Box = { name, initialized: false, refs: new Set() }; boxes.set(username, box); scheduleReapIfIdle(username); } diff --git a/apps/agent-worker/tests/unit/user-container.test.ts b/apps/agent-worker/tests/unit/user-container.test.ts index 08b36ad..7f6a7a3 100644 --- a/apps/agent-worker/tests/unit/user-container.test.ts +++ b/apps/agent-worker/tests/unit/user-container.test.ts @@ -83,6 +83,30 @@ describe('box registry', () => { ); }); + test('ensures an adopted box once before returning acquire handles', async () => { + const module = await load(); + const docker = await import('../../src/runtime/docker'); + vi.mocked(docker.listWorkspaceContainerNames).mockResolvedValueOnce([ + 'spoon-box-casey', + ]); + await module.reconcileExistingBoxes(); + + const first = await module.acquireUserBox({ + username: 'casey', + workdir: '/w', + containerHome: '/home/casey', + }); + const second = await module.acquireUserBox({ + username: 'casey', + workdir: '/w', + containerHome: '/home/casey', + }); + + expect(docker.ensureUserContainer).toHaveBeenCalledTimes(1); + expect(first.boxName).toBe('spoon-box-casey'); + expect(second.boxName).toBe('spoon-box-casey'); + }); + test('retries ensure after the first acquire fails', async () => { const module = await load(); const docker = await import('../../src/runtime/docker');