fix(gateway): surface clean channel exits

This commit is contained in:
Peter Steinberger
2026-04-28 01:58:47 +01:00
parent 53d213f9cc
commit a9bd8bb9b4
3 changed files with 45 additions and 0 deletions

View File

@@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- CLI/status: show skipped fast-path memory checks as `not checked` and report active custom memory plugin runtime status from `status --json --all` without requiring built-in `agents.defaults.memorySearch`, so plugins such as memory-lancedb-pro and memory-cms no longer look unavailable when their own runtime is healthy. Fixes #56968. Thanks @Tony-ooo and @aderius.
- Gateway/channels: record and log unexpected clean channel monitor exits so channels that return without throwing no longer appear stopped with no error. Fixes #73099. Thanks @balaji1968-kingler.
- Memory/LanceDB: let embedding config use provider-backed auth profiles, environment credentials, or provider config without a separate plugin `embedding.apiKey`, so OAuth-capable embedding providers can power auto-recall/capture. Fixes #68950. Thanks @malshaalan-ai.
- Plugins/hooks: time out never-settling `agent_end` observation hooks after 30 seconds and log the plugin failure, so hung embedding endpoints no longer leave memory capture silently pending forever. Fixes #65544. Thanks @ghoc0099.
- Gateway/config: serve runtime config schemas from the current plugin metadata snapshot and generated bundled channel schema metadata instead of rebuilding plugin channel config modules on every `config.get`/`config.schema`, preventing idle plugin-discovery CPU churn after upgrades. Fixes #73088. Thanks @sleitor and @geovansb.

View File

@@ -186,11 +186,47 @@ describe("server-channels auto restart", () => {
const account = snapshot.channelAccounts.discord?.[DEFAULT_ACCOUNT_ID];
expect(account?.running).toBe(false);
expect(account?.reconnectAttempts).toBe(11);
expect(account?.lastError).toBe("channel exited without an error");
await vi.advanceTimersByTimeAsync(200);
expect(startAccount).toHaveBeenCalledTimes(11);
});
it("records a clean channel monitor exit before auto-restart", async () => {
const startAccount = vi.fn(async () => {});
installTestRegistry(createTestPlugin({ startAccount }));
const manager = createManager();
await manager.startChannels();
await vi.advanceTimersByTimeAsync(0);
const snapshot = manager.getRuntimeSnapshot();
const account = snapshot.channelAccounts.discord?.[DEFAULT_ACCOUNT_ID];
expect(startAccount).toHaveBeenCalled();
expect(account?.running).toBe(false);
expect(account?.restartPending).toBe(true);
expect(account?.lastError).toBe("channel exited without an error");
});
it("does not record a clean-exit error for manual abort stops", async () => {
const startAccount = vi.fn(
async ({ abortSignal }: { abortSignal: AbortSignal }) =>
await new Promise<void>((resolve) => {
abortSignal.addEventListener("abort", () => resolve(), { once: true });
}),
);
installTestRegistry(createTestPlugin({ startAccount }));
const manager = createManager();
await manager.startChannels();
await manager.stopChannel("discord", DEFAULT_ACCOUNT_ID);
const snapshot = manager.getRuntimeSnapshot();
const account = snapshot.channelAccounts.discord?.[DEFAULT_ACCOUNT_ID];
expect(account?.running).toBe(false);
expect(account?.lastError).toBeNull();
});
it("does not auto-restart after manual stop during backoff", async () => {
const startAccount = vi.fn(async () => {});
installTestRegistry(

View File

@@ -509,6 +509,14 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
),
);
const trackedPromise = task
.then(() => {
if (abort.signal.aborted || manuallyStopped.has(rKey)) {
return;
}
const message = "channel exited without an error";
setRuntime(channelId, id, { accountId: id, lastError: message });
log.error?.(`[${id}] ${message}`);
})
.catch((err) => {
const message = formatErrorMessage(err);
setRuntime(channelId, id, { accountId: id, lastError: message });