fix: share one bounded lifetime across computer file operations

Propagate the admitted operation deadline through readiness, claims, and SSH commands. Keep only a bounded transport-settlement hold, and expire a short file waiter without cancelling shared SSH initialization.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-10 13:56:40 -05:00
1 parent 2c09c8c16d
commit 9c583bf39d
5 files changed
+188 -28

No files matched your search

@@ -23,14 +23,16 @@ export interface ComputerRepository {
agentId: string,
): Promise<"active" | "terminal" | "missing">;
}
export type ComputerOperationOptions = { deadlineMs?: number };
export interface ComputerBackend {
inspect(record: ComputerRecord): Promise<{
state: string;
snapshots: boolean;
stop: null | { id: string; status: string };
}>;
ready(record: ComputerRecord): Promise<void>;
claim(record: ComputerRecord): Promise<void>;
ready(record: ComputerRecord, options?: ComputerOperationOptions): Promise<void>;
claim(record: ComputerRecord, options?: ComputerOperationOptions): Promise<void>;
advance(record: ComputerRecord, owner: Owner): Promise<void>;
runnerPorts(record: ComputerRecord): Promise<RunnerPortAvailability>;
runner(record: ComputerRecord, options?: { control?: boolean }): Promise<CommandManagedRuntimeRunner>;
@@ -66,6 +68,6 @@ export interface ComputerBackend {
path: string,
): Promise<{ url: string; secretHeaders: Record<string, string> }>;
preview(record: ComputerRecord, port: number): Promise<{ url: string }>;
remote(record: ComputerRecord, input: Record<string, unknown>): Promise<any>;
remote(record: ComputerRecord, input: Record<string, unknown>, options?: ComputerOperationOptions): Promise<any>;
}
export type { Ledger };
@@ -512,6 +512,66 @@ describe("computer ownership", () => {
).resolves.toHaveProperty("owner");
expect(f.backend.stop).toHaveBeenCalledOnce();
});
it("shares one file budget across readiness, claim, and command without early retirement", async () => {
const f = fixture();
await f.attach();
const files = await f.service.files({ ...f.scope, agentId: "agent" });
const expectedDeadline = Date.parse("2026-10-10T12:02:00Z");
vi.mocked(f.backend.ready).mockImplementation(async (_record, options) => {
expect(options?.deadlineMs).toBe(expectedDeadline);
f.advance(90_000);
});
vi.mocked(f.backend.claim).mockImplementation(async (_record, options) => {
expect(options?.deadlineMs).toBe(expectedDeadline);
f.advance(20_000);
});
vi.mocked(f.backend.remote).mockImplementation(async (_record, _input, options) => {
expect(options?.deadlineMs).toBe(expectedDeadline);
f.advance(9999);
await f.service.reconcile();
expect(f.backend.stop).not.toHaveBeenCalled();
f.advance(1);
return {};
});
await files.stat("AGENTS.md");
f.advance(5001);
await f.service.reconcile();
expect(f.backend.stop).toHaveBeenCalledOnce();
});
it("does not launch file work after slow readiness consumes its budget", async () => {
const f = fixture();
await f.attach();
const files = await f.service.files({ ...f.scope, agentId: "agent" });
vi.mocked(f.backend.claim).mockClear();
vi.mocked(f.backend.ready).mockImplementation(async () => { f.advance(120_000); });
await expect(files.stat("AGENTS.md")).rejects.toThrow("timed out");
expect(f.backend.claim).not.toHaveBeenCalled();
expect(f.backend.remote).not.toHaveBeenCalled();
f.advance(5001);
await f.service.reconcile();
expect(f.backend.stop).toHaveBeenCalledOnce();
});
it("bounds an abandoned file hold and fences a late readiness continuation", async () => {
const f = fixture();
await f.attach();
const files = await f.service.files({ ...f.scope, agentId: "agent" });
let finish!: () => void;
let entered!: () => void;
const readyEntered = new Promise<void>(resolve => { entered = resolve; });
vi.mocked(f.backend.claim).mockClear();
vi.mocked(f.backend.ready).mockImplementation(() => new Promise<void>(resolve => { finish = resolve; entered(); }));
const operation = files.stat("AGENTS.md");
const rejected = expect(operation).rejects.toThrow("timed out");
await readyEntered;
f.advance(125_001);
await f.service.reconcile();
expect(f.backend.stop).toHaveBeenCalledOnce();
finish();
await rejected;
expect(f.backend.claim).not.toHaveBeenCalled();
expect(f.backend.remote).not.toHaveBeenCalled();
expect((await f.repository.get(f.scope)).ledger.owners.every(owner => owner.phase === "retired")).toBe(true);
});
it("keeps a short bounded file batch hold between editor operations", async () => {
const f = fixture();
await f.attach();
@@ -26,6 +26,8 @@ class ComputerSessionBusyError extends ComputerError {
// This is shutdown time only; admission and ordinary work stop at the warm deadline.
const RETIREMENT_GRACE_MS = 30_000;
const FILE_OPERATION_BUDGET_MS = 120_000;
const FILE_OPERATION_SETTLEMENT_MS = 5000;
type Scope = { companyId: string; environmentId: string };
export function createComputerService(
repository: ComputerRepository,
@@ -697,26 +699,39 @@ finally:
async function withFiles<T>(
input: Scope,
root: string,
fn: (record: ComputerRecord) => Promise<T>,
fn: (record: ComputerRecord, deadlineMs: number) => Promise<T>,
): Promise<T> {
const data = await admitRecord(input, (record) => {
const deadlineMs = now().getTime() + FILE_OPERATION_BUDGET_MS;
const owner: Owner = {
id: randomUUID(),
generation: 1,
kind: "file-operation",
phase: "active",
port: 0,
deadline: new Date(now().getTime() + 120_000).toISOString(),
// Transport cancellation has a bounded settlement window; it is not
// additional time in which commands may start or continue.
deadline: new Date(deadlineMs + FILE_OPERATION_SETTLEMENT_MS).toISOString(),
absoluteDeadline: null,
process: null,
};
record.ledger.owners.push(owner);
return { record: structuredClone(record), owner };
return { record: structuredClone(record), owner, deadlineMs };
});
const assertLive = async () => {
if (now().getTime() >= data.deadlineMs)
throw new ComputerError("provider_error", "Computer file operation timed out");
const current = await repository.get(input);
if (exactOwner(current, ref(data.record, data.owner)).phase !== "active")
throw new ComputerError("conflict", "Computer file operation is no longer active");
};
try {
await backend.ready(data.record);
await backend.claim(data.record);
return await fn(data.record);
await assertLive();
await backend.ready(data.record, { deadlineMs: data.deadlineMs });
await assertLive();
await backend.claim(data.record, { deadlineMs: data.deadlineMs });
await assertLive();
return await fn(data.record, data.deadlineMs);
} finally {
await repository.update(input, (record) => {
const owner = record.ledger.owners.find(
@@ -724,15 +739,18 @@ finally:
);
if (owner?.phase === "active") {
owner.phase = "warm";
owner.deadline = new Date(now().getTime() + 5000).toISOString();
owner.deadline = new Date(Math.min(
now().getTime() + FILE_OPERATION_SETTLEMENT_MS,
data.deadlineMs + FILE_OPERATION_SETTLEMENT_MS,
)).toISOString();
}
});
}
}
function fileAccess(input: Scope, root: string) {
const remote = (payload: Record<string, unknown>) =>
withFiles(input, root, (record) =>
backend.remote(record, { ...payload, root }),
withFiles(input, root, (record, deadlineMs) =>
backend.remote(record, { ...payload, root }, { deadlineMs }),
);
const readBytes = async (
path: string,