fix: preserve formatted channel startup logs

This commit is contained in:
Peter Steinberger
2026-05-02 05:05:02 +01:00
parent 614a294afa
commit 4cca1b2399
5 changed files with 55 additions and 9 deletions

View File

@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Gateway/logging: keep deferred channel startup logs on the subsystem logger, so Slack, Discord, Telegram, and voice-call startup messages keep timestamped prefixes. Thanks @vincentkoc.
- Discord/reactions: skip reaction listener registration when DMs and group DMs are disabled and every configured guild has `reactionNotifications: "off"`, avoiding needless reaction-event queue work. Fixes #47516. Thanks @x4v13r1120.
- CLI sessions: preserve explicit manual-attach reuse bindings so trusted CLI sessions are not invalidated on the first turn when auth, prompt, or MCP fingerprints drift. Fixes #75849. Thanks @alfredjbclaw.
- Telegram/streaming: keep partial preview streaming enabled for plain reply-to replies, disabling drafts only for real native quote excerpts that require Telegram quote parameters. Fixes #73505. Thanks @choury.

View File

@@ -307,6 +307,7 @@ export async function createVoiceCallRuntime(params: {
coreConfig,
fullConfig ?? (coreConfig as OpenClawConfig),
agentRuntime,
log,
);
if (realtimeProvider) {
const { RealtimeCallHandler } = await loadRealtimeHandler();

View File

@@ -35,6 +35,12 @@ const TRANSCRIPT_LOG_MAX_CHARS = 200;
type RealtimeTranscriptionRuntime = typeof import("./realtime-transcription.runtime.js");
type ResponseGeneratorModule = typeof import("./response-generator.js");
type Logger = {
info: (message: string) => void;
warn: (message: string) => void;
error: (message: string) => void;
debug?: (message: string) => void;
};
let realtimeTranscriptionRuntimePromise: Promise<RealtimeTranscriptionRuntime> | undefined;
let responseGeneratorModulePromise: Promise<ResponseGeneratorModule> | undefined;
@@ -158,6 +164,7 @@ export class VoiceCallWebhookServer {
private coreConfig: CoreConfig | null;
private fullConfig: OpenClawConfig | null;
private agentRuntime: CoreAgentDeps | null;
private logger: Logger;
private stopStaleCallReaper: (() => void) | null = null;
private readonly webhookInFlightLimiter = createWebhookInFlightLimiter();
@@ -175,6 +182,7 @@ export class VoiceCallWebhookServer {
coreConfig?: CoreConfig,
fullConfig?: OpenClawConfig,
agentRuntime?: CoreAgentDeps,
logger?: Logger,
) {
this.config = normalizeVoiceCallConfig(config);
this.manager = manager;
@@ -182,6 +190,12 @@ export class VoiceCallWebhookServer {
this.coreConfig = coreConfig ?? null;
this.fullConfig = fullConfig ?? null;
this.agentRuntime = agentRuntime ?? null;
this.logger = logger ?? {
info: console.log,
warn: console.warn,
error: console.error,
debug: console.debug,
};
}
/**
@@ -492,12 +506,12 @@ export class VoiceCallWebhookServer {
const url = this.resolveListeningUrl(bind, webhookPath);
this.listeningUrl = url;
this.startPromise = null;
console.log(`[voice-call] Webhook server listening on ${url}`);
this.logger.info(`[voice-call] Webhook server listening on ${url}`);
if (this.mediaStreamHandler) {
const address = this.server?.address();
const actualPort =
address && typeof address === "object" ? address.port : this.config.serve.port;
console.log(
this.logger.info(
`[voice-call] Media stream WebSocket on ws://${bind}:${actualPort}${streamPath}`,
);
}

View File

@@ -354,6 +354,26 @@ describe("server-channels auto restart", () => {
expect(ctx?.channelRuntime).not.toBe(channelRuntime);
});
it("creates formatted runtime and log sinks for channels loaded after manager construction", async () => {
const startAccount = vi.fn(async (_ctx: ChannelGatewayContext<TestAccount>) => {});
installTestRegistry(createTestPlugin({ id: "slack", startAccount }));
const channelLogs = {} as Record<ChannelId, SubsystemLogger>;
const channelRuntimeEnvs = {} as Record<ChannelId, RuntimeEnv>;
const manager = createChannelManager({
getRuntimeConfig: () => ({}),
channelLogs,
channelRuntimeEnvs,
});
await manager.startChannel("slack");
expect(startAccount).toHaveBeenCalledTimes(1);
const [ctx] = startAccount.mock.calls[0] ?? [];
expect(ctx?.log).toBe(channelLogs.slack);
expect(ctx?.runtime).toBe(channelRuntimeEnvs.slack);
expect((ctx?.log as SubsystemLogger | undefined)?.subsystem).toBe("channels/slack");
});
it("deduplicates concurrent start requests for the same account", async () => {
const startupGate = createDeferred();
const isConfigured = vi.fn(async () => {

View File

@@ -13,7 +13,7 @@ import { type BackoffPolicy, computeBackoff, sleepWithAbort } from "../infra/bac
import { createTaskScopedChannelRuntime } from "../infra/channel-runtime-context.js";
import { formatErrorMessage } from "../infra/errors.js";
import { resetDirectoryCache } from "../infra/outbound/target-resolver.js";
import type { createSubsystemLogger } from "../logging/subsystem.js";
import { createSubsystemLogger, runtimeForLogger } from "../logging/subsystem.js";
import { resolveAccountEntry, resolveNormalizedAccountEntry } from "../routing/account-lookup.js";
import {
DEFAULT_ACCOUNT_ID,
@@ -214,6 +214,14 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
const manuallyStopped = new Set<string>();
const restartKey = (channelId: ChannelId, accountId: string) => `${channelId}:${accountId}`;
const ensureChannelLog = (channelId: ChannelId): SubsystemLogger => {
channelLogs[channelId] ??= createSubsystemLogger("channels").child(channelId);
return channelLogs[channelId];
};
const ensureChannelRuntime = (channelId: ChannelId): RuntimeEnv => {
channelRuntimeEnvs[channelId] ??= runtimeForLogger(ensureChannelLog(channelId));
return channelRuntimeEnvs[channelId];
};
const resolveAccountHealthMonitorOverride = (
channelConfig: ChannelHealthMonitorConfig | undefined,
@@ -265,7 +273,7 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
// This call exists solely to fail closed if resolver-side config loading is broken.
plugin.config.resolveAccount(cfg, accountId);
} catch (err) {
channelLogs[channelId].warn?.(
ensureChannelLog(channelId).warn?.(
`[${channelId}:${accountId}] health-monitor: failed to resolve account; skipping monitor (${formatErrorMessage(err)})`,
);
return false;
@@ -388,7 +396,8 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
const abort = new AbortController();
store.aborts.set(id, abort);
let handedOffTask = false;
const log = channelLogs[channelId];
const log = ensureChannelLog(channelId);
const runtime = ensureChannelRuntime(channelId);
let scopedChannelRuntime: ReturnType<typeof createTaskScopedChannelRuntime> | null = null;
let channelRuntimeForTask: ChannelRuntimeSurface | undefined;
let stopApprovalBootstrap: () => Promise<void> = async () => {};
@@ -499,7 +508,7 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
cfg,
accountId: id,
account,
runtime: channelRuntimeEnvs[channelId],
runtime,
abortSignal: abort.signal,
log,
getStatus: () => getRuntime(channelId, id),
@@ -641,16 +650,17 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
}
manuallyStopped.add(restartKey(channelId, id));
abort?.abort();
const log = channelLogs[channelId];
const log = ensureChannelLog(channelId);
const runtime = ensureChannelRuntime(channelId);
if (plugin?.gateway?.stopAccount) {
const account = plugin.config.resolveAccount(cfg, id);
await plugin.gateway.stopAccount({
cfg,
accountId: id,
account,
runtime: channelRuntimeEnvs[channelId],
runtime,
abortSignal: abort?.signal ?? new AbortController().signal,
log: channelLogs[channelId],
log,
getStatus: () => getRuntime(channelId, id),
setStatus: (next) => setRuntime(channelId, id, next),
});