mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-28 13:47:12 +02:00
fix: queue session approvals
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+66
-20
@@ -26,6 +26,9 @@ import {
|
||||
} from '@shared/domain/pattern';
|
||||
import {
|
||||
approvalPolicyRequiresCheckpoint,
|
||||
dequeuePendingApprovalState,
|
||||
enqueuePendingApprovalState,
|
||||
listPendingApprovals,
|
||||
normalizeApprovalPolicy,
|
||||
resolvePendingApproval,
|
||||
type ApprovalDecision,
|
||||
@@ -512,6 +515,15 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
const session = this.requireSession(workspace, sessionId);
|
||||
const approval = session.pendingApproval;
|
||||
if (!approval || approval.id !== approvalId) {
|
||||
const queuedApproval = session.pendingApprovalQueue?.some((candidate) => candidate.id === approvalId);
|
||||
if (queuedApproval) {
|
||||
throw new Error(
|
||||
approval
|
||||
? `Approval "${approvalId}" is queued behind "${approval.id}" for session "${sessionId}". Resolve the active approval first.`
|
||||
: `Approval "${approvalId}" is queued but not active for session "${sessionId}".`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`Approval "${approvalId}" is not pending for session "${sessionId}".`);
|
||||
}
|
||||
|
||||
@@ -522,7 +534,7 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
|
||||
const resolvedAt = nowIso();
|
||||
const resolvedApproval = resolvePendingApproval(approval, decision, resolvedAt);
|
||||
session.pendingApproval = undefined;
|
||||
this.setSessionPendingApprovalState(session, dequeuePendingApprovalState(session, approvalId));
|
||||
session.updatedAt = resolvedAt;
|
||||
|
||||
const updatedRun = this.updateSessionRun(session, handle.requestId, (run) =>
|
||||
@@ -539,6 +551,11 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
await Promise.resolve(handle.resolve(decision));
|
||||
} catch (error) {
|
||||
const failedAt = nowIso();
|
||||
this.rejectPendingApprovals(
|
||||
session,
|
||||
failedAt,
|
||||
'Queued approval was cancelled because the run failed before it could resume.',
|
||||
);
|
||||
session.status = 'error';
|
||||
session.lastError = error instanceof Error ? error.message : String(error);
|
||||
session.updatedAt = failedAt;
|
||||
@@ -931,14 +948,10 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
resolve: (decision: ApprovalDecision) => void | Promise<void>,
|
||||
): Promise<void> {
|
||||
const session = this.requireSession(workspace, sessionId);
|
||||
if (session.pendingApproval) {
|
||||
throw new Error(`Session "${sessionId}" already has a pending approval.`);
|
||||
}
|
||||
|
||||
const pendingApproval =
|
||||
'type' in approval ? this.createPendingApprovalFromSidecarEvent(approval) : approval;
|
||||
|
||||
session.pendingApproval = pendingApproval;
|
||||
this.setSessionPendingApprovalState(session, enqueuePendingApprovalState(session, pendingApproval));
|
||||
session.updatedAt = pendingApproval.requestedAt;
|
||||
|
||||
const updatedRun = this.updateSessionRun(session, requestId, (run) =>
|
||||
@@ -971,6 +984,41 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
};
|
||||
}
|
||||
|
||||
private setSessionPendingApprovalState(
|
||||
session: SessionRecord,
|
||||
state: {
|
||||
pendingApproval?: PendingApprovalRecord;
|
||||
pendingApprovalQueue?: PendingApprovalRecord[];
|
||||
},
|
||||
): void {
|
||||
session.pendingApproval = state.pendingApproval;
|
||||
session.pendingApprovalQueue = state.pendingApprovalQueue;
|
||||
}
|
||||
|
||||
private rejectPendingApprovals(
|
||||
session: SessionRecord,
|
||||
failedAt: string,
|
||||
error: string,
|
||||
): string[] {
|
||||
const requestIds = new Set<string>();
|
||||
|
||||
for (const pendingApproval of listPendingApprovals(session)) {
|
||||
const requestId = this.findApprovalRequestId(session, pendingApproval.id);
|
||||
const rejectedApproval = resolvePendingApproval(pendingApproval, 'rejected', failedAt, error);
|
||||
|
||||
if (requestId) {
|
||||
requestIds.add(requestId);
|
||||
this.updateSessionRun(session, requestId, (run) =>
|
||||
upsertRunApprovalEvent(run, rejectedApproval));
|
||||
}
|
||||
|
||||
this.pendingApprovalHandles.delete(pendingApproval.id);
|
||||
}
|
||||
|
||||
this.setSessionPendingApprovalState(session, {});
|
||||
return [...requestIds];
|
||||
}
|
||||
|
||||
private async awaitFinalResponseApproval(
|
||||
workspace: WorkspaceState,
|
||||
sessionId: string,
|
||||
@@ -1192,32 +1240,30 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
let changed = false;
|
||||
|
||||
for (const session of workspace.sessions) {
|
||||
const pendingApproval = session.pendingApproval;
|
||||
if (!pendingApproval) {
|
||||
const pendingApprovals = listPendingApprovals(session);
|
||||
if (pendingApprovals.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
changed = true;
|
||||
const failedAt = nowIso();
|
||||
const error = 'Pending approval was interrupted because Eryx restarted before a decision was recorded.';
|
||||
const requestId = this.findApprovalRequestId(session, pendingApproval.id);
|
||||
const rejectedApproval = resolvePendingApproval(pendingApproval, 'rejected', failedAt, error);
|
||||
|
||||
session.pendingApproval = undefined;
|
||||
const requestIds = this.rejectPendingApprovals(session, failedAt, error);
|
||||
session.status = 'error';
|
||||
session.lastError = error;
|
||||
session.updatedAt = failedAt;
|
||||
|
||||
if (!requestId) {
|
||||
continue;
|
||||
if (requestIds.length === 0) {
|
||||
const fallbackRequestId = session.runs.find((run) => run.status === 'running')?.requestId;
|
||||
if (fallbackRequestId) {
|
||||
requestIds.push(fallbackRequestId);
|
||||
}
|
||||
}
|
||||
|
||||
this.updateSessionRun(session, requestId, (run) =>
|
||||
failSessionRunRecord(
|
||||
upsertRunApprovalEvent(run, rejectedApproval),
|
||||
failedAt,
|
||||
error,
|
||||
));
|
||||
for (const requestId of requestIds) {
|
||||
this.updateSessionRun(session, requestId, (run) =>
|
||||
failSessionRunRecord(run, failedAt, error));
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { normalizeSessionRunRecords } from '@shared/domain/runTimeline';
|
||||
import { normalizeSessionToolingSelection, normalizeWorkspaceSettings } from '@shared/domain/tooling';
|
||||
import {
|
||||
normalizeApprovalPolicy,
|
||||
normalizePendingApproval,
|
||||
normalizePendingApprovalState,
|
||||
} from '@shared/domain/approval';
|
||||
import { createWorkspaceSeed, type WorkspaceState } from '@shared/domain/workspace';
|
||||
import { nowIso } from '@shared/utils/ids';
|
||||
@@ -72,7 +72,10 @@ export class WorkspaceRepository {
|
||||
...session,
|
||||
runs: normalizeSessionRunRecords(session.runs),
|
||||
tooling: normalizeSessionToolingSelection(session.tooling),
|
||||
pendingApproval: normalizePendingApproval(session.pendingApproval),
|
||||
...normalizePendingApprovalState({
|
||||
pendingApproval: session.pendingApproval,
|
||||
pendingApprovalQueue: session.pendingApprovalQueue,
|
||||
}),
|
||||
})),
|
||||
settings: normalizeWorkspaceSettings(stored.settings),
|
||||
selectedProjectId: projects.some((project) => project.id === stored.selectedProjectId)
|
||||
|
||||
Reference in New Issue
Block a user