fix: repair Grok CLI dashboard chat (routing + stale bundle)

Grok CLI chat failed two different ways depending on the surface:

1. Default (no-project) chat errored with "requires the bundled Grok CLI
   runtime". The default ChatManager was handed a bare PluginLoader, but Grok
   routing (deriveGrokRuntimeHintForNoVisibleKey -> resolveRuntime) needs a
   PluginRunner's getRuntimeById/createRuntimeContext; the unguarded call threw
   "getRuntimeById is not a function". New resolveChatManagerPluginRunner()
   prefers the engine's PluginRunner (same runner the project-scoped path uses),
   falling back to the loader only in UI-only mode.

2. Project-scoped chat returned empty replies. The CLI-bundled Grok plugin
   (packages/cli/dist/plugins/.../bundled.js, gitignored) was stale vs the
   FN-7796 single-JSON adapter source; the running server loads that bundle,
   not the plugin's own dist. `pnpm build` regenerates it. Noted in the
   changeset that the freshness guard only warns and the dev prebuild does not
   rebuild the CLI tsup bundle.

Verified end-to-end on a live dashboard: both default and project-scoped
grok-cli/grok-4.5 chats now stream thinking + text.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-10 15:36:25 -07:00
parent 8f7089ecc9
commit a9d5c0f763
3 changed files with 91 additions and 2 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Fix Grok CLI chat returning errors or empty replies in the dashboard.
category: fix
dev: Two independent defects. (1) The default (no-project) ChatManager received a bare PluginLoader as its runner; Grok CLI routing (deriveGrokRuntimeHintForNoVisibleKey → resolveRuntime) calls getRuntimeById/createRuntimeContext, which only exist on PluginRunner, so a grok-cli/* chat with no Fusion-visible GROK_API_KEY threw "getRuntimeById is not a function" and surfaced the misleading "requires the bundled Grok CLI runtime" error. New resolveChatManagerPluginRunner(options) prefers the engine's PluginRunner (the runner the project-scoped chat path already uses), falling back to the loader only in UI-only mode. (2) The CLI-bundled Grok plugin (packages/cli/dist/plugins/fusion-plugin-grok-runtime/bundled.js) was stale relative to the FN-7796 single-JSON adapter source, so project-scoped grok chat produced empty replies; a `pnpm build` (tsup) regenerates it. The bundled-plugin freshness guard only warns — the dev prebuild does not rebuild the CLI tsup bundle.

View File

@@ -0,0 +1,50 @@
import { describe, expect, it } from "vitest";
import { resolveChatManagerPluginRunner } from "../server.js";
/*
FNXC:GrokCliRouting 2026-07-10-00:00:
Regression guard for the "grok chat not working" default-chat failure. The
default (no-project) ChatManager must receive a real PluginRunner (with
`getRuntimeById`/`createRuntimeContext`) for Grok CLI runtime resolution — a
bare PluginLoader lacks `getRuntimeById`, so `deriveGrokRuntimeHintForNoVisibleKey`
threw "getRuntimeById is not a function" → the misleading "requires the bundled
Grok CLI runtime" error. `resolveChatManagerPluginRunner` must prefer the
engine's PluginRunner and fall back to the loader only when no engine exists.
*/
describe("resolveChatManagerPluginRunner", () => {
const bareLoader = { getPluginRoutes: () => [] };
const engineRunner = {
getPluginRoutes: () => [],
getRuntimeById: () => ({ pluginId: "fusion-plugin-grok-runtime", runtime: {} }),
createRuntimeContext: async () => ({}),
};
it("prefers the engine's PluginRunner over the bare loader when an engine is present", () => {
const engine = { getPluginRunner: () => engineRunner } as never;
const resolved = resolveChatManagerPluginRunner({
engine,
pluginRunner: bareLoader as never,
});
expect(resolved).toBe(engineRunner);
// The chosen runner must expose runtime resolution — the exact capability
// the bare loader lacks and that Grok CLI routing depends on.
expect(typeof (resolved as { getRuntimeById?: unknown })?.getRuntimeById).toBe("function");
});
it("falls back to options.pluginRunner in UI-only mode (no engine)", () => {
const resolved = resolveChatManagerPluginRunner({
engine: undefined,
pluginRunner: bareLoader as never,
});
expect(resolved).toBe(bareLoader);
});
it("falls back to options.pluginRunner when the engine exposes no runner", () => {
const engine = { getPluginRunner: () => undefined } as never;
const resolved = resolveChatManagerPluginRunner({
engine,
pluginRunner: bareLoader as never,
});
expect(resolved).toBe(bareLoader);
});
});

View File

@@ -769,6 +769,26 @@ export function wireCliRelaunchListener(options: {
});
}
/*
FNXC:GrokCliRouting 2026-07-10-00:00:
Select the PluginRunner the default (no-project) ChatManager uses for runtime
resolution. Grok CLI routing (deriveGrokRuntimeHintForNoVisibleKey → resolveRuntime)
calls `getRuntimeById` and `createRuntimeContext`, which exist only on a real
PluginRunner — a bare PluginLoader (what `options.pluginRunner` is in the CLI
`dashboard` command) lacks them, so a `grok-cli/*` chat with no Fusion-visible
GROK_API_KEY threw "getRuntimeById is not a function" and surfaced the misleading
"requires the bundled Grok CLI runtime" error. Prefer the engine's PluginRunner
(the same runner the project-scoped chat path already uses via
engine.getPluginRunner()); fall back to `options.pluginRunner` only in UI-only
mode where no engine exists.
*/
export function resolveChatManagerPluginRunner(
options?: Pick<ServerOptions, "engine" | "pluginRunner">,
): ServerOptions["pluginRunner"] {
const engineRunner = options?.engine?.getPluginRunner?.();
return (engineRunner as ServerOptions["pluginRunner"] | undefined) ?? options?.pluginRunner;
}
export function createServer(store: TaskStore, options?: ServerOptions): ReturnType<typeof express> {
// Register the universal post-create hook so every task-creation path
// (HTTP routes, CLI, pi extension, mission triage, etc.) triggers
@@ -1374,12 +1394,24 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
// Create AgentStore for chat prompt enrichment (initialized lazily by ChatManager)
const chatAgentStore = new AgentStore({ rootDir: store.getFusionDir() });
// Create ChatManager for AI chat message handling
// Create ChatManager for AI chat message handling.
/*
FNXC:GrokCliRouting 2026-07-10-00:00:
The default (no-project) ChatManager must receive a real PluginRunner — not the
bare PluginLoader passed as `options.pluginRunner`. Grok CLI routing
(deriveGrokRuntimeHintForNoVisibleKey → resolveRuntime) calls `getRuntimeById`
and `createRuntimeContext`, which exist only on PluginRunner; a PluginLoader
lacks them, so a `grok-cli/*` chat with no visible GROK_API_KEY threw
"getRuntimeById is not a function" → the misleading "requires the bundled Grok
CLI runtime" error. Prefer the engine's PluginRunner (the same runner the
project-scoped chat path already uses via engine.getPluginRunner()), falling
back to the loader only in UI-only mode where no engine exists.
*/
const chatManager = options?.chatManager ?? new ChatManager(
chatStore,
store.getRootDir(),
chatAgentStore,
options?.pluginRunner,
resolveChatManagerPluginRunner(options),
() => store.getSettings(),
options?.engine?.getMessageStore(),
store,