Bug report
Summary
Since #1998, the Hermes launcher prefers the pure-ESM dist/bridge.mjs entry over dist/bridge.cjs. However, bridge.mts only starts the bridge-status.json status heartbeat in its stdio branch (if (!args.daemon)). The --daemon branch never calls markConnected() / startHeartbeat() — so no daemon ever refreshes the status file.
The legacy bridge.cts daemon path has both calls right after the viewer port binds; the ESM successor lost them in translation.
Impact
Any --daemon spawn writes no status updates. Readers (health endpoint, external monitors) apply the 20s staleness rule to whatever snapshot is on disk and permanently report:
status: "disconnected"
lastError: "Hermes bridge heartbeat is stale"
…while the bridge is actually healthy: RPC (POST /api/v1/rpc) keeps working normally because it never touches status tracking. Monitoring built on the health endpoint therefore shows a dead bridge during fully functional operation.
Repro
node dist/bridge.mjs --agent=hermes --daemon
- Watch
<home-root>/bridge-status.json: once stale from a previous run, its mtime/content never changes again after daemon start (no initial write either)
- ~20s later, the health endpoint reports
bridge.status: disconnected, "Hermes bridge heartbeat is stale" — indefinitely
Observed in production after a v2.0.7 → v2.0.16 jump: the status file froze at the pre-upgrade daemon's last write, and every respawned daemon (fresh PID) left it untouched.
Root cause
apps/memos-local-plugin/bridge.mts — markConnected() / startHeartbeat() appear only inside if (!args.daemon). The if (args.daemon) block has none. Compare bridge.cts, which calls both after successful viewer bind in daemon mode.
Suggested fix
Mirror the .cts daemon path — two lines after the bind-retry loop succeeds:
bridgeStatus?.markConnected();
bridgeHeartbeat = bridgeStatus?.startHeartbeat();
PR incoming with the fix plus a source-level regression guard test (same pattern as the existing bridge-startup-ordering.test.ts).
Bug report
Summary
Since #1998, the Hermes launcher prefers the pure-ESM
dist/bridge.mjsentry overdist/bridge.cjs. However,bridge.mtsonly starts thebridge-status.jsonstatus heartbeat in its stdio branch (if (!args.daemon)). The--daemonbranch never callsmarkConnected()/startHeartbeat()— so no daemon ever refreshes the status file.The legacy
bridge.ctsdaemon path has both calls right after the viewer port binds; the ESM successor lost them in translation.Impact
Any
--daemonspawn writes no status updates. Readers (health endpoint, external monitors) apply the 20s staleness rule to whatever snapshot is on disk and permanently report:…while the bridge is actually healthy: RPC (
POST /api/v1/rpc) keeps working normally because it never touches status tracking. Monitoring built on the health endpoint therefore shows a dead bridge during fully functional operation.Repro
node dist/bridge.mjs --agent=hermes --daemon<home-root>/bridge-status.json: once stale from a previous run, its mtime/content never changes again after daemon start (no initial write either)bridge.status: disconnected,"Hermes bridge heartbeat is stale"— indefinitelyObserved in production after a v2.0.7 → v2.0.16 jump: the status file froze at the pre-upgrade daemon's last write, and every respawned daemon (fresh PID) left it untouched.
Root cause
apps/memos-local-plugin/bridge.mts—markConnected()/startHeartbeat()appear only insideif (!args.daemon). Theif (args.daemon)block has none. Comparebridge.cts, which calls both after successful viewer bind in daemon mode.Suggested fix
Mirror the
.ctsdaemon path — two lines after the bind-retry loop succeeds:PR incoming with the fix plus a source-level regression guard test (same pattern as the existing
bridge-startup-ordering.test.ts).