fix: preserve paired completion errors without leases (#217)

This commit is contained in:
Eyejoker
2026-06-03 08:59:51 +08:00
committed by GitHub
parent 32468e0214
commit 0cbb7e3b9c
2 changed files with 147 additions and 90 deletions

View File

@@ -1446,6 +1446,34 @@ describe('paired execution context', () => {
}); });
}); });
describe('paired execution context completion lease cleanup', () => {
beforeEach(() => {
vi.resetAllMocks();
});
it('propagates completion handler errors when no execution lease was reserved', () => {
const transitionError = new Error('transition failed without lease');
vi.mocked(db.getPairedTaskById).mockReturnValue(
buildPairedTask({
status: 'merge_ready',
}),
);
vi.mocked(db.updatePairedTaskIfUnchanged).mockImplementationOnce(() => {
throw transitionError;
});
expect(() =>
completePairedExecutionContext({
taskId: 'task-1',
role: 'owner',
status: 'failed',
summary: 'push failed',
}),
).toThrow('transition failed without lease');
expect(db.releasePairedTaskExecutionLease).not.toHaveBeenCalled();
});
});
describe('paired execution context reviewer failures', () => { describe('paired execution context reviewer failures', () => {
beforeEach(() => { beforeEach(() => {
vi.resetAllMocks(); vi.resetAllMocks();

View File

@@ -619,15 +619,18 @@ export function preparePairedExecutionContext(args: {
}; };
} }
export function completePairedExecutionContext(args: { type CompletePairedExecutionContextArgs = {
taskId: string; taskId: string;
role: PairedRoomRole; role: PairedRoomRole;
status: 'succeeded' | 'failed'; status: 'succeeded' | 'failed';
runId?: string; runId?: string;
summary?: string | null; summary?: string | null;
}): void { };
export function completePairedExecutionContext(
args: CompletePairedExecutionContextArgs,
): void {
const { taskId, role, status } = args; const { taskId, role, status } = args;
let completionError: unknown;
logger.info( logger.info(
{ {
taskId, taskId,
@@ -639,6 +642,28 @@ export function completePairedExecutionContext(args: {
); );
try { try {
completePairedExecutionContextBody(args);
} catch (error) {
releaseExecutionLeaseAfterCompletion({
taskId,
role,
runId: args.runId,
completionError: error,
});
throw error;
}
releaseExecutionLeaseAfterCompletion({
taskId,
role,
runId: args.runId,
});
}
function completePairedExecutionContextBody(
args: CompletePairedExecutionContextArgs,
): void {
const { taskId, role, status } = args;
const task = getPairedTaskById(taskId); const task = getPairedTaskById(taskId);
if (!task) { if (!task) {
return; return;
@@ -711,28 +736,32 @@ export function completePairedExecutionContext(args: {
if (role === 'arbiter') { if (role === 'arbiter') {
handleArbiterCompletion({ task, taskId, summary: args.summary }); handleArbiterCompletion({ task, taskId, summary: args.summary });
} }
} catch (error) { }
completionError = error;
throw error; function releaseExecutionLeaseAfterCompletion(args: {
} finally { taskId: string;
role: PairedRoomRole;
runId?: string;
completionError?: unknown;
}): void {
if (!args.runId) { if (!args.runId) {
return; return;
} }
try { try {
releasePairedTaskExecutionLease({ taskId, runId: args.runId }); releasePairedTaskExecutionLease({ taskId: args.taskId, runId: args.runId });
} catch (releaseError) { } catch (releaseError) {
logger.error( logger.error(
{ {
taskId, taskId: args.taskId,
role, role: args.role,
runId: args.runId, runId: args.runId,
releaseError, releaseError,
}, },
'Failed to release paired task execution lease after completion', 'Failed to release paired task execution lease after completion',
); );
if (!completionError) { if (!args.completionError) {
throw releaseError; throw releaseError;
} }
} }
}
} }