fix: settle Pi child capacity reservations

This commit is contained in:
2026-08-23 13:34:41 +08:00
parent 3de85d61d7
commit a10b98e484
5 changed files with 275 additions and 33 deletions

View File

@@ -58,7 +58,7 @@ export interface PiSubagentDispatchOptions {
export interface PiSubagentSchedulerOptions {
openChild(input: PiSubagentChildOpenInput): Promise<PiSubagentChild>;
processBudget: PiProcessBudget;
reclaimProcessCapacity?(): Promise<boolean>;
reclaimProcessCapacity?(signal?: AbortSignal): Promise<boolean>;
createId?: (kind: 'dispatch' | 'task') => string;
}
@@ -216,7 +216,7 @@ export class PiSubagentScheduler {
private readonly openChild: PiSubagentSchedulerOptions['openChild'];
private readonly processBudget: PiProcessBudget;
private readonly childPermits: FifoSemaphore;
private readonly reclaimProcessCapacity: (() => Promise<boolean>) | undefined;
private readonly reclaimProcessCapacity: ((signal?: AbortSignal) => Promise<boolean>) | undefined;
private readonly createId: NonNullable<PiSubagentSchedulerOptions['createId']>;
private readonly dispatches = new Map<string, DispatchRecord>();
private readonly parentDispatches = new Map<string, Set<string>>();
@@ -353,12 +353,7 @@ export class PiSubagentScheduler {
let child: PiSubagentChild | undefined;
try {
releaseChild = await this.childPermits.acquire(record.controller.signal);
const budgetWasFull = this.processBudget.activeCount >= this.processBudget.maxProcesses;
const processLeaseFlight = this.processBudget.acquire(record.controller.signal, 'child');
if (budgetWasFull && this.reclaimProcessCapacity) {
await this.reclaimProcessCapacity();
}
processLease = await processLeaseFlight;
processLease = await this.acquireProcessLease(record.controller.signal);
if (record.controller.signal.aborted) throw new PiSubagentChildError('SUBAGENT_ABORTED');
projected.status = 'running';
this.emit(details, onUpdate);
@@ -395,6 +390,33 @@ export class PiSubagentScheduler {
}
}
private async acquireProcessLease(signal: AbortSignal): Promise<PiProcessLease> {
const reservation = new AbortController();
const abortReservation = () => reservation.abort();
signal.addEventListener('abort', abortReservation, { once: true });
if (signal.aborted) reservation.abort();
const budgetWasFull = this.processBudget.activeCount >= this.processBudget.maxProcesses;
const processLeaseFlight = this.processBudget.acquire(reservation.signal, 'child');
try {
if (budgetWasFull && this.reclaimProcessCapacity) {
const outcome = await Promise.race([
processLeaseFlight.then((lease) => ({ lease })),
this.reclaimProcessCapacity(reservation.signal).then(() => null),
]);
if (outcome) return outcome.lease;
}
return await processLeaseFlight;
} catch (error) {
reservation.abort();
const orphanedLease = await processLeaseFlight.catch(() => undefined);
orphanedLease?.release();
throw error;
} finally {
signal.removeEventListener('abort', abortReservation);
reservation.abort();
}
}
private markRemaining(
details: SubagentDetailsV1,
from: number,