Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/memos-local-plugin/bridge.cts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,11 @@ async function main(): Promise<void> {
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(
Expand Down
16 changes: 15 additions & 1 deletion apps/memos-local-plugin/core/pipeline/memory-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 },
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
});
});