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:
7
.changeset/grok-cli-chat-routing-and-stale-bundle.md
Normal file
7
.changeset/grok-cli-chat-routing-and-stale-bundle.md
Normal 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.
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user