Plugin loading is the most fragile part of QNTX startup. Loom has intermittently failed to start across many restarts. When it fails, there is zero trace in the structured log file (tmp/qntx-*.log). The only evidence exists in terminal output — which is gone the moment the terminal scrolls or the session ends.
This session proved:
plugin_manager_is_nil: true at every startup, then nothing until bandaid code logs "Plugin loading completed (async)" after the fact.waitForPlugin but discovered the code path is impossible to trigger because launchPlugin already waits 2s for port announcement. The real failure happens upstream."No port announcement from plugin, assuming requested port") also only go to terminal.The root cause is not a timeout value or missing retry logic. The root cause is split observability: two loggers, one blind.
There are two loggers:
logger.Logger (global) — writes to terminal only (os.Stdout). Created in init().serverLogger (server) — writes to terminal + WebSocket + file. Created in server/init.go:createServerLogger().The plugin loader uses logger.Logger.Named("plugin-loader") — terminal only. Everything it logs is invisible in the structured log file.
The server logger adds three things on top of the base logger:
logger.Logger)tmp/qntx-*.log)The file core has no dependency on the server. It only needs a log path, which comes from config. There is no reason it can't be part of the global logger from the start.
The WebSocket core does depend on the server — but that's the browser UI log panel, not the structured log file.
logger.Logger earlyIn main.go, after loading config but before plugin loading starts:
server/init.go)logger.Logger with a tee of console + fileThen pluginLogger = logger.Logger.Named("plugin-loader") automatically inherits file output.
createServerLogger should add the WebSocket core on top of the already-file-enabled global logger. It should not recreate the file core — that's already there.
The code in cmd/qntx/main.go that logs plugin results through defaultServer.GetLogger() after async loading completes — this becomes unnecessary because m.logger (the plugin loader) now writes to the file directly.
Similarly, server/server.go:GetLogger() was added solely for this workaround.
logger/logger.go — add InitializeWithFile(logPath) or modify Initialize to accept optional file pathcmd/qntx/main.go — call file-enabled init before initializePluginRegistry(), remove bandaid loggingserver/init.go — createServerLogger reuses global logger's cores instead of rebuildingserver/server.go — evaluate if GetLogger() is still neededmake testmake dev, then check tmp/qntx-*.log for:
"Discovered plugin port from stdout")waitForPlugin attempts ("Plugin ready", "Plugin not ready")