Skip to content

fix(plugin): initialize file logging from the ESM bridge entry - #2465

Open
akushonkamen wants to merge 1 commit into
MemTensor:mainfrom
akushonkamen:fix/bridge-esm-init-logging
Open

akushonkamen wants to merge 1 commit into
MemTensor:mainfrom
akushonkamen:fix/bridge-esm-init-logging

Conversation

@akushonkamen

Copy link
Copy Markdown

Fixes #2399


Summary

The two hand-copied bridge entries drifted apart when the standalone-logging
flag (initLogging) landed in dev-v2.0.25: the legacy CJS entry
(bridge.cts) passes initLogging: true to bootstrapMemoryCoreFull, but the
ESM entry (bridge.mts) — the entry bridge_client.py actually prefers —
does not. Result: every bridge started from a current build never constructs
the logger, so config.logging is silently inert end to end (console channel,
timezone, level, channels, and all file sinks under <home>/logs).

This PR adds the missing initLogging: true (plus the explanatory comment
already present in bridge.cts) and an integration regression that spawns
dist/bridge.mjs and asserts the file sinks appear.

Root cause

// before — bridge.mts (the actual entry: bridge_client.py resolves
// dist/bridge.mjs > dist/bridge.cjs > bridge.mts > bridge.cts)
const { core, config, home } = await bootstrapMemoryCoreFull({
  agent: args.agent,
  namespace: { agentKind: args.agent, profileId: "default" },
  autoRecovery: args.agent !== "hermes" || !args.daemon,
  pkgVersion,
  hostLlmBridge: args.daemon ? null : lazyHostLlmBridge,
  home: resolvedHome,
});                                // ← no initLogging

bootstrapMemoryCoreFull gates its only initLogger call on the option
(core/pipeline/memory-core.ts):

/**
 * When true, initialize the global logger from `config.logging` (…). 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;
…
if (options.initLogging) {
  initLogger(config, home);
}

bridge.cts honors the contract (and comments it); bridge.mts was never
updated when the flag was introduced, and the option is optional/falsy-skip, so
nothing warns. The file transports (core/logger/index.ts →
FileRotatingTransport) are the only thing that creates <home>/logs, and
they are constructed eagerly inside initLogger — hence "no logs directory,
ever" from the packaged entry, exactly as reported.

Behavior-change notes

For existing users this restores the documented behavior rather than adding
overhead: after this change a packaged bridge actually produces the sinks that
config.logging already promises (default logging.file.enabled: true —
memos.log, error.log, audit.log, events.jsonl, llm.jsonl,
perf.jsonl under <home>/logs). Nothing else about logging changes:

  • bridge.cts — already correct, untouched.
  • The embedded host (adapters/deepseek-harness/index.ts, initLogging: false) is intentionally untouched — embedded hosts keeping control of
    logging is the documented design.
  • The unrotated daemon-start.log mentioned in the issue is a separate code
    path outside initLogger and is out of scope here.

Tests

New regression case in
apps/memos-local-plugin/tests/integration/bridge-esm-entry.test.ts (the
existing #1736 harness: spawn dist/bridge.mjs, temporary home, grace-period
SIGTERM): after startup, logs/memos.log and logs/error.log must exist under
the runtime home. The transports open their files eagerly during initLogger,
so the assertion is deterministic. As in the #1736 case, an environment-level
missing-native-binding failure is tolerated (unrelated to this issue).

Red→green evidence:

  • Red — unfixed main @ a7367d0, built, run by hand and via the new
    test:
    $ node dist/bridge.mjs --agent=hermes --no-viewer --home=<tmp>
    … bridge: stdio mode running without viewer (agent=hermes)   # starts fine
    $ ls <tmp>
    bridge-status.json  data  memos-local        # no logs/
    × expected a file sink at <tmp>/logs/memos.log after startup …
      expected false to be true
    Test Files  1 failed (1)
    Tests  1 failed | 1 passed (2)
    
  • Green — same flow after the one-line fix (rebuilt):
    ✓ tests/integration/bridge-esm-entry.test.ts (2 tests) 555ms
    Test Files  1 passed (1)
    Tests  2 passed (2)
    $ ls <tmp>/logs
    audit.log  error.log  events.jsonl  llm.jsonl  memos.log  perf.jsonl
    $ head -1 <tmp>/logs/memos.log
    {"pid":…,"src":"ts","seq":1,"tz":"UTC","ts":…,"level":"warn","kind":"app",
     "channel":"core.pipeline.bootstrap", …}
    

Validation (all offline, no LLM key / Docker / Neo4j / Qdrant):

  • npx vitest run tests/integration/bridge-esm-entry.test.ts — 2/2
  • npx vitest run tests/unit/bridge-status.test.ts — 3/3
  • npm run test:unit — 181 files, 1589 passed, 2 skipped (zero regressions)
  • npm run lint (tsc -p tsconfig.json --noEmit) — clean

Fixes MemTensor#2399

## Summary

- pass `initLogging: true` from the ESM entry (`bridge.mts`) to
  `bootstrapMemoryCoreFull`, aligning it with the legacy CJS entry
  (`bridge.cts`), which already passes the flag
- add an integration regression asserting that a bridge started from
  `dist/bridge.mjs` creates the `config.logging` file sinks
  (`logs/memos.log`, `logs/error.log`) under the runtime home

## Why

`bootstrapMemoryCoreFull` only initializes the global logger when
`options.initLogging` is set — the option's documented contract is that
standalone bridge entries own their stdio and must opt in, while
embedded hosts leave it false. `bridge.cts` passes the flag;
`bridge.mts` never did. Because `bridge_client.py` resolves the entry as
`dist/bridge.mjs` > `dist/bridge.cjs` > `bridge.mts` > `bridge.cts`,
every bridge launched from a current build took the ESM path and skipped
`initLogger` entirely: `config.logging` (timezone, level, channels, and
the file sinks under `<home>/logs`) was silently inert — no file sink
was ever created and nothing warned about it.

## Validation

- new integration case in
  `apps/memos-local-plugin/tests/integration/bridge-esm-entry.test.ts`:
  red on unfixed `main` (no `logs/memos.log` after startup), green after
  the fix
- build-level check (`node dist/bridge.mjs --agent=hermes --no-viewer
  --home=<tmp>`): before the fix `<tmp>/logs` was never created; after
  the fix it contains all six sinks (`memos.log`, `error.log`,
  `audit.log`, `events.jsonl`, `llm.jsonl`, `perf.jsonl`)
- `npx vitest run tests/unit/bridge-status.test.ts` — 3/3
- `npm run test:unit` — 181 files, 1589 passed, 2 skipped
- `npm run lint` (`tsc -p tsconfig.json --noEmit`) — clean

The embedded-host path (`adapters/deepseek-harness`, which intentionally
passes `initLogging: false`) is untouched.
@Memtensor-AI Memtensor-AI added area:plugin OpenClaw & Hermes status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Oct 8, 2026
@Memtensor-AI

Copy link
Copy Markdown
Collaborator

🤖 Open Code Review

Target: PR #2465
Task: c68f0ef5eef74277
Base: main
Head: fix/bridge-esm-init-logging

✅ OpenCodeReview: Review skipped: no items were selected.

Generated by cloud-assistant via Open Code Review.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

⚠️ Automated Test Results: ENV ISSUE

The test environment encountered an issue that requires manual attention.

Details: Environment preparation failed before any gating tests executed. Failed scopes: memos_local_plugin
Branch: fix/bridge-esm-init-logging

@akushonkamen

Copy link
Copy Markdown
Author

Heads-up on CI: the autotest bot reports the same ENV ISSUE here as on #2464 and #2466:

  • Scope: memos_local_plugin — environment preparation failed before any gating tests executed, so this is an infra issue, not a code failure.
  • It reproduces across all three open PRs, and on fix(plugin): stop clamping storage list queries to 500 rows #2464 the re-run on the new head (cd70df6) is also ENV ISSUE — so it does not look transient or PR-specific.
  • Could a CI admin check the memos_local_plugin scope environment, or manually trigger a rerun once it's healthy? Happy to help debug the scope setup if needed.

Memtensor-AI pushed a commit to Memtensor-AI/MemOS that referenced this pull request Oct 8, 2026
…or#2470)

`_rebuild_if_stale` in the Hermes adapter compared TypeScript source
timestamps against `dist/bridge.cjs`, but both spawn paths prefer
`dist/bridge.mjs` since the ESM migration (MemTensor#1736 / MemTensor#1998). A stale
`bridge.mjs` next to a fresh `bridge.cjs` therefore slipped past the
guard and the daemon kept booting the stale ESM artifact without any
warning — exactly the failure mode MemTensor#2399 / MemTensor#2465 triggered.

Mirror `_bridge_script()`'s compiled-entry precedence and apply the
stricter variant: rebuild when any compiled entry on disk is older
than the newest `*.ts` source, treat a missing `dist/` as stale, and
name the stale artifacts in the log line.

Covered by 9 new unittest cases in
`tests/python/test_rebuild_if_stale.py`, including the headline
"stale .mjs, fresh .cjs, newer source" scenario the issue describes.
Full plugin python suite (141 tests) and ruff check / format pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:plugin OpenClaw & Hermes status:in-progress Someone or AI is working on it | 人工或 AI 正在处理

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] bridge.mts never passes initLogging - the packaged ESM entry writes no log files

3 participants