fix(pi): expire terminal subagents automatically
This commit is contained in:
@@ -69,6 +69,7 @@
|
|||||||
modules.agents.tools.gitea-axi.enable = true;
|
modules.agents.tools.gitea-axi.enable = true;
|
||||||
modules.agents.pi.enable = true;
|
modules.agents.pi.enable = true;
|
||||||
modules.agents.pi.subagents.maxConcurrent = 8;
|
modules.agents.pi.subagents.maxConcurrent = 8;
|
||||||
|
modules.agents.pi.subagents.recentTerminalTtlMs = 15 * 60 * 1000;
|
||||||
|
|
||||||
modules.desktop.enable = true;
|
modules.desktop.enable = true;
|
||||||
modules.desktop.obsidian.enable = true;
|
modules.desktop.obsidian.enable = true;
|
||||||
|
|||||||
@@ -335,29 +335,31 @@ test("clearTerminal returns only removed terminal ids", async () => {
|
|||||||
assert.equal(supervisor.status(running.id).state, "running");
|
assert.equal(supervisor.status(running.id).state, "running");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("terminal records stay listed past ttl and remain retrievable until cleared", async () => {
|
test("terminal records expire after ttl while active children remain", async () => {
|
||||||
const runner = new FakeRunner();
|
const runner = new FakeRunner();
|
||||||
const supervisor = new Supervisor(runner, "/tmp", { recentTerminalTtlMs: 5 });
|
const supervisor = new Supervisor(runner, "/tmp", { recentTerminalTtlMs: 5 });
|
||||||
const completed = await spawnStarted(supervisor, "one");
|
const completed = await spawnStarted(supervisor, "one");
|
||||||
const failed = await spawnStarted(supervisor, "two");
|
const failed = await spawnStarted(supervisor, "two");
|
||||||
|
const running = await spawnStarted(supervisor, "three");
|
||||||
|
|
||||||
runner.starts[0].events.completed("one done", "agent_settled");
|
runner.starts[0].events.completed("one done", "agent_settled");
|
||||||
runner.starts[1].events.failed("two failed");
|
runner.starts[1].events.failed("two failed");
|
||||||
await sleep(10);
|
|
||||||
|
|
||||||
const listedIds = supervisor.list().map((status) => status.id);
|
|
||||||
assert.ok(listedIds.includes(completed.id));
|
|
||||||
assert.ok(listedIds.includes(failed.id));
|
|
||||||
assert.equal(supervisor.result(completed.id).result, "one done");
|
assert.equal(supervisor.result(completed.id).result, "one done");
|
||||||
assert.equal(supervisor.result(failed.id).error, "two failed");
|
assert.equal(supervisor.result(failed.id).error, "two failed");
|
||||||
|
assert.equal(supervisor.status(running.id).state, "running");
|
||||||
|
|
||||||
(supervisor as Supervisor & { clearTerminal(): void }).clearTerminal();
|
await sleep(20);
|
||||||
|
|
||||||
const afterClearIds = supervisor.list().map((status) => status.id);
|
const listedIds = supervisor.list().map((status) => status.id);
|
||||||
assert.equal(afterClearIds.includes(completed.id), false);
|
assert.equal(listedIds.includes(completed.id), false);
|
||||||
assert.equal(afterClearIds.includes(failed.id), false);
|
assert.equal(listedIds.includes(failed.id), false);
|
||||||
|
assert.equal(listedIds.includes(running.id), true);
|
||||||
assert.throws(() => supervisor.status(completed.id), /unknown subagent id/);
|
assert.throws(() => supervisor.status(completed.id), /unknown subagent id/);
|
||||||
|
assert.throws(() => supervisor.status(failed.id), /unknown subagent id/);
|
||||||
|
assert.throws(() => supervisor.result(completed.id), /unknown subagent id/);
|
||||||
assert.throws(() => supervisor.result(failed.id), /unknown subagent id/);
|
assert.throws(() => supervisor.result(failed.id), /unknown subagent id/);
|
||||||
|
assert.equal(supervisor.status(running.id).state, "running");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("zero recent terminal ttl does not hide terminal statuses", async () => {
|
test("zero recent terminal ttl does not hide terminal statuses", async () => {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface RunningChild {
|
|||||||
handle?: ChildHandle;
|
handle?: ChildHandle;
|
||||||
startTimer?: ReturnType<typeof setTimeout>;
|
startTimer?: ReturnType<typeof setTimeout>;
|
||||||
runTimer?: ReturnType<typeof setTimeout>;
|
runTimer?: ReturnType<typeof setTimeout>;
|
||||||
|
expiryTimer?: ReturnType<typeof setTimeout>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SupervisorOptions {
|
interface SupervisorOptions {
|
||||||
@@ -99,6 +100,7 @@ export class Supervisor {
|
|||||||
for (const [id, child] of this.children) {
|
for (const [id, child] of this.children) {
|
||||||
if (selectedIds && !selectedIds.includes(id)) continue;
|
if (selectedIds && !selectedIds.includes(id)) continue;
|
||||||
if (!isTerminal(child.record.status.state)) continue;
|
if (!isTerminal(child.record.status.state)) continue;
|
||||||
|
this.clearTimer(child, "expiryTimer");
|
||||||
cleared.push(id);
|
cleared.push(id);
|
||||||
this.children.delete(id);
|
this.children.delete(id);
|
||||||
}
|
}
|
||||||
@@ -159,6 +161,7 @@ export class Supervisor {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
for (const child of this.children.values()) this.clearTimer(child, "expiryTimer");
|
||||||
}
|
}
|
||||||
|
|
||||||
private createChild(request: SpawnRequest): SpawnAccepted {
|
private createChild(request: SpawnRequest): SpawnAccepted {
|
||||||
@@ -250,7 +253,10 @@ export class Supervisor {
|
|||||||
this.recordActivity(record, "completed", now);
|
this.recordActivity(record, "completed", now);
|
||||||
record.status.stopReason = stopReason;
|
record.status.stopReason = stopReason;
|
||||||
record.status.resultAvailable = true;
|
record.status.resultAvailable = true;
|
||||||
if (child) this.emitMilestone(child, "completed");
|
if (child) {
|
||||||
|
this.armTerminalExpiry(child);
|
||||||
|
this.emitMilestone(child, "completed");
|
||||||
|
}
|
||||||
this.pumpQueue();
|
this.pumpQueue();
|
||||||
},
|
},
|
||||||
failed: (error) => this.fail(record, error),
|
failed: (error) => this.fail(record, error),
|
||||||
@@ -269,7 +275,10 @@ export class Supervisor {
|
|||||||
this.recordActivity(record, "failed", now);
|
this.recordActivity(record, "failed", now);
|
||||||
record.status.error = error;
|
record.status.error = error;
|
||||||
record.status.stopReason = "failed";
|
record.status.stopReason = "failed";
|
||||||
if (child) this.emitMilestone(child, "failed");
|
if (child) {
|
||||||
|
this.armTerminalExpiry(child);
|
||||||
|
this.emitMilestone(child, "failed");
|
||||||
|
}
|
||||||
this.pumpQueue();
|
this.pumpQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,6 +292,7 @@ export class Supervisor {
|
|||||||
child.record.status.lastEventAt = now;
|
child.record.status.lastEventAt = now;
|
||||||
this.recordActivity(child.record, state, now);
|
this.recordActivity(child.record, state, now);
|
||||||
child.record.status.stopReason = reason;
|
child.record.status.stopReason = reason;
|
||||||
|
this.armTerminalExpiry(child);
|
||||||
this.emitMilestone(child, state);
|
this.emitMilestone(child, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,12 +319,26 @@ export class Supervisor {
|
|||||||
this.pumpQueue();
|
this.pumpQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private armTerminalExpiry(child: RunningChild) {
|
||||||
|
const ttl = this.options.recentTerminalTtlMs;
|
||||||
|
if (ttl === undefined || ttl <= 0) return;
|
||||||
|
this.clearTimer(child, "expiryTimer");
|
||||||
|
child.expiryTimer = setTimeout(() => {
|
||||||
|
child.expiryTimer = undefined;
|
||||||
|
const id = child.record.status.id;
|
||||||
|
if (this.children.get(id) !== child || !isTerminal(child.record.status.state)) return;
|
||||||
|
this.children.delete(id);
|
||||||
|
this.emitChange();
|
||||||
|
}, ttl);
|
||||||
|
child.expiryTimer.unref?.();
|
||||||
|
}
|
||||||
|
|
||||||
private clearTimers(child: RunningChild) {
|
private clearTimers(child: RunningChild) {
|
||||||
this.clearTimer(child, "startTimer");
|
this.clearTimer(child, "startTimer");
|
||||||
this.clearTimer(child, "runTimer");
|
this.clearTimer(child, "runTimer");
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearTimer(child: RunningChild, key: "startTimer" | "runTimer") {
|
private clearTimer(child: RunningChild, key: "startTimer" | "runTimer" | "expiryTimer") {
|
||||||
const timer = child[key];
|
const timer = child[key];
|
||||||
if (!timer) return;
|
if (!timer) return;
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
|
|||||||
Reference in New Issue
Block a user