fix: clean up interrupted session state on restart

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-28 16:22:12 +01:00
co-authored by Copilot
parent b946359c69
commit 3c57cb6ded
2 changed files with 288 additions and 15 deletions
+28 -15
View File
@@ -156,6 +156,11 @@ function isSidecarStoppedBeforeCompletionError(error: unknown): error is Error {
return error instanceof Error && error.message === SIDECAR_STOPPED_BEFORE_COMPLETION_MESSAGE;
}
const INTERRUPTED_RUN_ERROR =
'This session was interrupted because Aryx restarted while a run was in progress.';
const INTERRUPTED_APPROVAL_ERROR =
'Pending approval was interrupted because Aryx restarted before a decision was recorded.';
export class AryxAppService extends EventEmitter<AppServiceEvents> {
private readonly workspaceRepository = new WorkspaceRepository();
private readonly sidecar = new SidecarClient();
@@ -195,7 +200,7 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|| didSyncProjectTooling
|| didPruneSelections
|| didPruneApprovalTools
|| this.failInterruptedPendingApprovals(this.workspace)
|| this.cleanupInterruptedSessions(this.workspace)
) {
await this.workspaceRepository.save(this.workspace);
}
@@ -2015,33 +2020,41 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
this.emit('session-event', event);
}
private failInterruptedPendingApprovals(workspace: WorkspaceState): boolean {
private cleanupInterruptedSessions(workspace: WorkspaceState): boolean {
let changed = false;
for (const session of workspace.sessions) {
const pendingApprovals = listPendingApprovals(session);
if (pendingApprovals.length === 0) {
const hasPendingUserInput = session.pendingUserInput !== undefined;
const failedRequestIds = new Set(
session.runs
.filter((run) => run.status === 'running')
.map((run) => run.requestId),
);
if (pendingApprovals.length === 0 && !hasPendingUserInput && failedRequestIds.size === 0) {
continue;
}
changed = true;
const failedAt = nowIso();
const error = 'Pending approval was interrupted because Aryx restarted before a decision was recorded.';
const requestIds = this.rejectPendingApprovals(session, failedAt, error);
session.status = 'error';
session.lastError = error;
session.updatedAt = failedAt;
if (requestIds.length === 0) {
const fallbackRequestId = session.runs.find((run) => run.status === 'running')?.requestId;
if (fallbackRequestId) {
requestIds.push(fallbackRequestId);
if (pendingApprovals.length > 0) {
for (const requestId of this.rejectPendingApprovals(session, failedAt, INTERRUPTED_APPROVAL_ERROR)) {
failedRequestIds.add(requestId);
}
}
for (const requestId of requestIds) {
if (session.pendingUserInput) {
this.pendingUserInputHandles.delete(session.pendingUserInput.id);
session.pendingUserInput = undefined;
}
session.status = 'error';
session.lastError = INTERRUPTED_RUN_ERROR;
session.updatedAt = failedAt;
for (const requestId of failedRequestIds) {
this.updateSessionRun(session, requestId, (run) =>
failSessionRunRecord(run, failedAt, error));
failSessionRunRecord(run, failedAt, INTERRUPTED_RUN_ERROR));
}
}