diff --git a/apps/memos-local-plugin/bridge.cts b/apps/memos-local-plugin/bridge.cts index 81848acf7..1d01b2140 100644 --- a/apps/memos-local-plugin/bridge.cts +++ b/apps/memos-local-plugin/bridge.cts @@ -252,6 +252,11 @@ async function main(): Promise { pkgVersion, hostLlmBridge: args.daemon ? null : lazyHostLlmBridge, home: resolvedHome, + // Standalone bridge owns its stdio — initialize the logger from + // config.logging (timezone, level, channels, file sinks). Without this the + // logger stays on the bootstrap console default (tz pinned to "UTC"), which + // makes logging.timezone inert in the daemon. + initLogging: true, }); const telemetry = new Telemetry( diff --git a/apps/memos-local-plugin/core/pipeline/memory-core.ts b/apps/memos-local-plugin/core/pipeline/memory-core.ts index b4e331c71..3e9541561 100644 --- a/apps/memos-local-plugin/core/pipeline/memory-core.ts +++ b/apps/memos-local-plugin/core/pipeline/memory-core.ts @@ -75,7 +75,7 @@ import type { import type { ResolvedConfig, ResolvedHome } from "../config/index.js"; import { loadConfig, resolveHome, SECRET_FIELD_PATHS } from "../config/index.js"; import { feedbackText, runFeedbackExperience } from "../experience/feedback-builder.js"; -import { rootLogger } from "../logger/index.js"; +import { initLogger, rootLogger } from "../logger/index.js"; import type { Logger } from "../logger/types.js"; import { openDb } from "../storage/connection.js"; import { runMigrations } from "../storage/migrator.js"; @@ -143,6 +143,13 @@ export interface BootstrapOptions { hostLlmBridge?: HostLlmBridge | null; /** Optional telemetry instance for ARMS RUM reporting. */ telemetry?: import("../telemetry/index.js").Telemetry | null; + /** + * When true, initialize the global logger from `config.logging` (timezone, + * level, channels, file/audit/llm/perf/events sinks). The standalone daemon + * (`bridge.cts`) owns its stdio and must set this; embedded plugin hosts + * leave it false so the host keeps control of logging. + */ + initLogging?: boolean; } export interface BootstrapResult { @@ -182,6 +189,13 @@ export async function bootstrapMemoryCoreFull( : await loadConfig(home); const config = configResult.config; + // Standalone daemon: wire the global logger from config (timezone, level, + // channels, file sinks) before anything logs. Embedded hosts skip this and + // keep their own logger. Idempotent — re-init swaps the active root in place. + if (options.initLogging) { + initLogger(config, home); + } + const log = rootLogger.child({ channel: "core.pipeline.bootstrap", ctx: { agent: options.agent }, diff --git a/apps/memos-local-plugin/tests/unit/pipeline/bootstrap-logging.test.ts b/apps/memos-local-plugin/tests/unit/pipeline/bootstrap-logging.test.ts new file mode 100644 index 000000000..89bdd2091 --- /dev/null +++ b/apps/memos-local-plugin/tests/unit/pipeline/bootstrap-logging.test.ts @@ -0,0 +1,87 @@ +/** + * Bootstrap logger initialization. + * + * The standalone daemon (`bridge.cts`) resolves config inside + * `bootstrapMemoryCoreFull` but historically never called `initLogger`, so the + * active logger stayed on the `bootstrapConsoleOnly()` default with `tz` pinned + * to "UTC". That made `logging.timezone` (and the rest of the `logging.*` + * block) inert in the daemon. These tests pin the opt-in wiring. + */ +import { afterEach, describe, expect, it } from "vitest"; + +import { makeTmpHome, type TmpHomeContext } from "../../helpers/tmp-home.js"; +import { + initTestLogger, + memoryBuffer, + rootLogger, +} from "../../../core/logger/index.js"; +import type { MemoryCore } from "../../../agent-contract/memory-core.js"; + +describe("bootstrapMemoryCoreFull logger init", () => { + let home: TmpHomeContext | null = null; + let core: MemoryCore | null = null; + + afterEach(async () => { + if (core) await core.shutdown(); + if (home) await home.cleanup(); + core = null; + home = null; + initTestLogger(); + }); + + it("initializes the active logger from config when initLogging is set", async () => { + const { bootstrapMemoryCoreFull } = await import( + "../../../core/pipeline/memory-core.js" + ); + home = await makeTmpHome({ + agent: "openclaw", + configYaml: ` +logging: + timezone: America/Los_Angeles +`, + }); + + const result = await bootstrapMemoryCoreFull({ + agent: "openclaw", + home: home.home, + config: home.config, + pkgVersion: "bootstrap-test", + initLogging: true, + }); + core = result.core; + + rootLogger.child({ channel: "core.session" }).info("bootstrap.tz"); + await rootLogger.flush(); + + expect(memoryBuffer().tail({ limit: 1 }).at(0)?.tz).toBe( + "America/Los_Angeles", + ); + }); + + it("leaves the logger untouched when initLogging is not requested", async () => { + const { bootstrapMemoryCoreFull } = await import( + "../../../core/pipeline/memory-core.js" + ); + home = await makeTmpHome({ + agent: "openclaw", + configYaml: ` +logging: + timezone: America/Los_Angeles +`, + }); + + const result = await bootstrapMemoryCoreFull({ + agent: "openclaw", + home: home.home, + config: home.config, + pkgVersion: "bootstrap-test", + }); + core = result.core; + + rootLogger.child({ channel: "core.session" }).info("bootstrap.default"); + await rootLogger.flush(); + + // Embedded-plugin path: host owns logging, so the default UTC logger stays. + expect(memoryBuffer().tail({ limit: 1 }).at(0)?.tz).toBe("UTC"); + }); +});