fix(gateway): clean up retired channel lifecycles

This commit is contained in:
pashpashpash
2026-04-28 11:15:34 -04:00
parent 7f1fe7731e
commit fb4f561d23
2 changed files with 39 additions and 5 deletions

View File

@@ -309,19 +309,33 @@ describe("server-channels auto restart", () => {
it("force-retires a hung channel task so recovery can start a fresh lifecycle", async () => {
const statusSetters: Array<(next: ChannelAccountSnapshot) => void> = [];
const startAccount = vi.fn(async ({ setStatus }: ChannelGatewayContext<TestAccount>) => {
statusSetters.push(setStatus);
await new Promise<void>(() => {});
});
const channelRuntime = createRuntimeChannel();
const contextKey = {
channelId: "discord",
accountId: DEFAULT_ACCOUNT_ID,
capability: "test-lifecycle",
};
const startAccount = vi.fn(
async ({ setStatus, channelRuntime }: ChannelGatewayContext<TestAccount>) => {
const lifecycle = statusSetters.length + 1;
statusSetters.push(setStatus);
channelRuntime?.runtimeContexts.register({
...contextKey,
context: { lifecycle },
});
await new Promise<void>(() => {});
},
);
installTestRegistry(
createTestPlugin({
startAccount,
}),
);
const manager = createManager();
const manager = createManager({ channelRuntime });
await manager.startChannels();
await Promise.resolve();
expect(channelRuntime.runtimeContexts.get(contextKey)).toEqual({ lifecycle: 1 });
const stopTask = manager.stopChannel("discord", DEFAULT_ACCOUNT_ID, {
forceRetireOnTimeout: true,
@@ -335,9 +349,11 @@ describe("server-channels auto restart", () => {
expect(account?.connected).toBe(false);
expect(account?.activeRuns).toBe(0);
expect(account?.lastError).toContain("stale lifecycle force-retired");
expect(channelRuntime.runtimeContexts.get(contextKey)).toBeUndefined();
await manager.startChannel("discord", DEFAULT_ACCOUNT_ID);
await Promise.resolve();
expect(channelRuntime.runtimeContexts.get(contextKey)).toEqual({ lifecycle: 2 });
expect(startAccount).toHaveBeenCalledTimes(2);
statusSetters[1]?.({

View File

@@ -39,6 +39,7 @@ type ChannelRuntimeStore = {
aborts: Map<string, AbortController>;
starting: Map<string, Promise<void>>;
tasks: Map<string, Promise<unknown>>;
taskCleanups: Map<string, () => Promise<void>>;
runtimes: Map<string, ChannelAccountSnapshot>;
};
@@ -61,6 +62,7 @@ function createRuntimeStore(): ChannelRuntimeStore {
aborts: new Map(),
starting: new Map(),
tasks: new Map(),
taskCleanups: new Map(),
runtimes: new Map(),
};
}
@@ -423,6 +425,10 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
log.error?.(`[${id}] ${label}: ${formatErrorMessage(error)}`);
}
};
const cleanupRetiredLifecycle = async () => {
await cleanupTaskScopedApprovalRuntime("channel lifecycle retirement cleanup failed");
};
store.taskCleanups.set(id, cleanupRetiredLifecycle);
try {
const account = plugin.config.resolveAccount(cfg, id);
@@ -542,6 +548,9 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
})
.finally(async () => {
await cleanupTaskScopedApprovalRuntime("channel cleanup failed");
if (store.taskCleanups.get(id) === cleanupRetiredLifecycle) {
store.taskCleanups.delete(id);
}
if (isCurrentLifecycle()) {
setRuntime(channelId, id, {
accountId: id,
@@ -627,6 +636,9 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
if (!handedOffTask && store.aborts.get(id) === abort) {
store.aborts.delete(id);
}
if (!handedOffTask && store.taskCleanups.get(id) === cleanupRetiredLifecycle) {
store.taskCleanups.delete(id);
}
}
}),
);
@@ -700,6 +712,11 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
if (store.tasks.get(id) === task) {
store.tasks.delete(id);
}
const cleanupRetiredTask = store.taskCleanups.get(id);
if (cleanupRetiredTask) {
store.taskCleanups.delete(id);
await cleanupRetiredTask();
}
setRuntime(channelId, id, {
accountId: id,
running: false,
@@ -722,6 +739,7 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
}
store.aborts.delete(id);
store.tasks.delete(id);
store.taskCleanups.delete(id);
setRuntime(channelId, id, {
accountId: id,
running: false,