fix(worker): make user-box lifecycle concurrency-safe

This commit is contained in:
Gabriel Brown
2026-07-10 16:54:55 -04:00
parent daec4d6dfa
commit 9e7a8722f3
6 changed files with 203 additions and 41 deletions
+11 -11
View File
@@ -16,6 +16,7 @@ import { api } from '@spoon/backend/convex/_generated/api.js';
import type { NormalizedAgentEvent } from './agent-events';
import type { OpenCodeSession } from './opencode-session';
import type { BoxHandle } from './user-container';
import { normalizeCodexJsonLine } from './agent-events';
import { prepareCodexWorkspaceFiles } from './codex-runtime';
import { env } from './env';
@@ -41,11 +42,7 @@ import {
stopWorkspaceContainer,
streamExecInContainer,
} from './runtime/docker';
import {
acquireUserBox,
releaseUserBox,
runningBoxUsernames,
} from './user-container';
import { acquireUserBox, runningBoxUsernames } from './user-container';
import { fetchUserEnvironment, materializeUserHome } from './user-environment';
type Claim = {
@@ -111,6 +108,7 @@ type ActiveWorkspace = {
repoDir: string;
// Phase 2: the per-user box container this thread execs into.
boxName: string;
boxHandle: BoxHandle;
githubToken: string;
redact: (value: string) => string;
runtimeMode?: 'opencode_server' | 'codex_exec' | 'legacy_cli';
@@ -1323,7 +1321,7 @@ const runClaim = async (claim: Claim) => {
...claim.secrets.map((secret) => secret.value),
].filter(Boolean);
const redact = createRedactor(secretValues);
let acquiredBoxUser: string | undefined;
let acquiredBoxHandle: BoxHandle | undefined;
try {
if ((claim.job.runtime ?? 'opencode') !== 'opencode') {
throw new Error('Legacy OpenAI direct jobs are no longer supported.');
@@ -1354,12 +1352,13 @@ const runClaim = async (claim: Claim) => {
// Start (or reuse) the persistent per-user box that this thread — and the
// terminal — exec into. It mounts the home, so the clone below is visible.
const boxName = await acquireUserBox({
const boxHandle = await acquireUserBox({
username,
workdir: homeDir,
containerHome,
});
acquiredBoxUser = username;
acquiredBoxHandle = boxHandle;
const boxName = boxHandle.boxName;
const repoDir = await cloneRepository({
workdir: checkoutParent,
@@ -1381,6 +1380,7 @@ const runClaim = async (claim: Claim) => {
containerRepo,
repoDir,
boxName,
boxHandle,
githubToken,
redact,
};
@@ -1460,7 +1460,7 @@ const runClaim = async (claim: Claim) => {
).catch((stopError: unknown) => {
console.error(stopError);
});
if (acquiredBoxUser) releaseUserBox(acquiredBoxUser);
acquiredBoxHandle?.release();
}
};
@@ -1883,7 +1883,7 @@ export const openWorkspacePullRequest = async (jobId: string) => {
activeWorkspaces.delete(jobId);
// The persistent per-user home + ~/Code checkouts survive across sessions;
// release the box (reaped once no other thread/terminal holds it).
releaseUserBox(workspace.username);
workspace.boxHandle.release();
return {
pullRequestUrl: pullRequest.html_url,
pullRequestNumber: pullRequest.number,
@@ -1900,7 +1900,7 @@ export const stopWorkspace = async (jobId: string) => {
activeWorkspaces.delete(jobId);
// The persistent per-user home + ~/Code checkouts survive across sessions;
// release the box (reaped once no other thread/terminal holds it).
releaseUserBox(workspace.username);
workspace.boxHandle.release();
return { success: true };
};