Summary
The logging system uses module-level state (loggingState object and externalTransports Set) that can be duplicated when plugins are loaded via jiti, causing registered log transports from plugins to be invisible to the main process.
This is architecturally identical to the issue fixed in #5190 for diagnostic events.
Steps to reproduce
- Create a plugin that registers a custom log transport:
// In plugin loaded via jiti
import { registerLogTransport } from "openclaw/plugin-sdk";
registerLogTransport((logObj) => {
console.log("Custom transport:", logObj);
});
- Configure the plugin in
plugins.entries and enable it
- Trigger logging from the main gateway process
- Observe that the custom transport is never called
Expected behavior
Plugin-registered log transports should receive log records emitted by the main process, similar to how diagnostic event listeners work after the #5190 fix.
Actual behavior
When jiti loads the plugin, it creates a separate module cache. The plugin adds its transport to the plugin module's externalTransports Set, but the main process iterates over its own (empty) externalTransports Set. The two module instances never share state.
Affected code locations:
src/logging/state.ts (line 1-17): Module-level loggingState object
src/logging/logger.ts (line 40): Module-level externalTransports Set
src/logging/logger.ts (line 195-204): registerLogTransport() function
src/logging/logger.ts (line 111-113): Transport iteration in buildLogger()
Environment
- Clawdbot version: 2026.2.1
- OS: macOS / Linux
- Install method: pnpm / npm
Logs or screenshots
Debug evidence (similar to #5190):
// In src/logging/logger.ts
export function registerLogTransport(transport: LogTransport): () => void {
console.error('[DEBUG] Adding transport. Current count:', externalTransports.size);
externalTransports.add(transport);
console.error('[DEBUG] After add:', externalTransports.size);
// ...
}
function buildLogger(settings: ResolvedSettings): TsLogger<LogObj> {
console.error('[DEBUG] Building logger, transports:', externalTransports.size);
// ...
}
Expected output:
- Plugin calls
registerLogTransport(): count 0 → 1 ✓
- Gateway calls
buildLogger(): transports = 1 ✓
Actual output:
- Plugin calls
registerLogTransport(): count 0 → 1 ✓
- Gateway calls
buildLogger(): transports = 0 ✗
Proposed solution
Use Symbol.for("openclaw.logging.state") as the global registry key (matching the approach in #5190):
// src/logging/state.ts
const GLOBAL_KEY = Symbol.for("openclaw.logging.state");
type LoggingGlobalState = {
cachedLogger: unknown;
cachedSettings: unknown;
// ... other fields
externalTransports: Set<LogTransport>;
};
function getGlobalLoggingState(): LoggingGlobalState {
const g = globalThis as typeof globalThis & { [GLOBAL_KEY]?: LoggingGlobalState };
if (!g[GLOBAL_KEY]) {
g[GLOBAL_KEY] = {
// ... initialize all fields
externalTransports: new Set(),
};
}
return g[GLOBAL_KEY];
}
export const loggingState = getGlobalLoggingState();
This ensures all module instances (main process + jiti-loaded plugins) share the same state object.
Additional context
Currently, no plugins register custom log transports, so this is a latent bug rather than an active failure. However, consistency with the diagnostic events fix (#5190) is important for maintainability.
Summary
The logging system uses module-level state (
loggingStateobject andexternalTransportsSet) that can be duplicated when plugins are loaded via jiti, causing registered log transports from plugins to be invisible to the main process.This is architecturally identical to the issue fixed in #5190 for diagnostic events.
Steps to reproduce
plugins.entriesand enable itExpected behavior
Plugin-registered log transports should receive log records emitted by the main process, similar to how diagnostic event listeners work after the #5190 fix.
Actual behavior
When jiti loads the plugin, it creates a separate module cache. The plugin adds its transport to the plugin module's
externalTransportsSet, but the main process iterates over its own (empty)externalTransportsSet. The two module instances never share state.Affected code locations:
src/logging/state.ts(line 1-17): Module-levelloggingStateobjectsrc/logging/logger.ts(line 40): Module-levelexternalTransportsSetsrc/logging/logger.ts(line 195-204):registerLogTransport()functionsrc/logging/logger.ts(line 111-113): Transport iteration inbuildLogger()Environment
Logs or screenshots
Debug evidence (similar to #5190):
Expected output:
registerLogTransport(): count 0 → 1 ✓buildLogger(): transports = 1 ✓Actual output:
registerLogTransport(): count 0 → 1 ✓buildLogger(): transports = 0 ✗Proposed solution
Use
Symbol.for("openclaw.logging.state")as the global registry key (matching the approach in #5190):This ensures all module instances (main process + jiti-loaded plugins) share the same state object.
Additional context
Currently, no plugins register custom log transports, so this is a latent bug rather than an active failure. However, consistency with the diagnostic events fix (#5190) is important for maintainability.