feat(pi): expire recent subagent statuses

This commit was merged in pull request #38.
This commit is contained in:
2026-08-01 16:47:34 -04:00
parent 3c4eaec76b
commit e143495d6c
5 changed files with 138 additions and 2 deletions

View File

@@ -24,6 +24,7 @@ interface RunningChild {
interface SupervisorOptions {
maxConcurrent?: number;
recentTerminalLimit?: number;
recentTerminalTtlMs?: number;
timeouts?: {
startMs?: number;
runMs?: number;
@@ -47,6 +48,7 @@ export class Supervisor {
private readonly children = new Map<string, RunningChild>();
private readonly queue: RunningChild[] = [];
private readonly waiters = new Set<() => void>();
private recentTerminalTimer?: ReturnType<typeof setTimeout>;
constructor(
private readonly runner: ChildRunner,
@@ -76,6 +78,7 @@ export class Supervisor {
const active = statuses.filter((status) => !isTerminal(status.state));
const terminal = statuses
.filter((status) => isTerminal(status.state))
.filter((status) => this.isRecentTerminal(status))
.sort((a, b) => Date.parse(b.completedAt ?? b.startedAt) - Date.parse(a.completedAt ?? a.startedAt))
.slice(0, this.options.recentTerminalLimit ?? 10);
return [...active, ...terminal];
@@ -134,6 +137,7 @@ export class Supervisor {
}
async shutdown(): Promise<void> {
this.clearRecentTerminalTimer();
await Promise.allSettled(
[...this.children.values()].map(async (child) => {
if (!isTerminal(child.record.status.state)) {
@@ -142,6 +146,7 @@ export class Supervisor {
}
}),
);
this.clearRecentTerminalTimer();
}
private createChild(request: SpawnRequest): SpawnAccepted {
@@ -340,6 +345,39 @@ export class Supervisor {
private emitChange() {
this.options.onChange?.(this.list());
for (const waiter of this.waiters) waiter();
this.scheduleRecentTerminalExpiry();
}
private scheduleRecentTerminalExpiry() {
this.clearRecentTerminalTimer();
const ttl = this.options.recentTerminalTtlMs;
if (ttl === undefined || ttl <= 0) return;
const now = Date.now();
const nextExpiryMs = [...this.children.values()]
.map((child) => child.record.status)
.filter((status) => isTerminal(status.state))
.map((status) => Date.parse(status.completedAt ?? status.startedAt))
.filter((completed) => Number.isFinite(completed))
.map((completed) => completed + ttl - now)
.filter((remaining) => remaining > 0)
.sort((a, b) => a - b)[0];
if (nextExpiryMs === undefined) return;
this.recentTerminalTimer = setTimeout(() => this.emitChange(), nextExpiryMs + 1);
}
private clearRecentTerminalTimer() {
if (!this.recentTerminalTimer) return;
clearTimeout(this.recentTerminalTimer);
this.recentTerminalTimer = undefined;
}
private isRecentTerminal(status: SubagentStatus): boolean {
const ttl = this.options.recentTerminalTtlMs;
if (ttl === undefined) return true;
if (ttl <= 0) return false;
const completed = Date.parse(status.completedAt ?? status.startedAt);
if (!Number.isFinite(completed)) return true;
return Date.now() - completed <= ttl;
}
private waitReady(ids: string[], mode: SubagentWaitMode): boolean {