PluginStore.updatePluginState rejects same-state transitions, so the
autoload at startup error'd with "Invalid state transition from
started to started" for every plugin whose persisted state was
"started" from a previous container generation. The new process has
no in-memory instance yet, so the right thing is to flip the
persisted state back to "installed" before loadAllPlugins() walks
the registry; loadPlugin() then drives the state machine forward to
"started" cleanly.
This unblocks the plugin route mount for telemetry-watcher.
The plugin route mount in createApiRoutes runs once at server start,
iterating pluginLoader.getPluginRoutes() to bind handlers and to
register their paths as daemon-auth exempt. Without an autoload step
the loader is empty at mount time, so plugin routes are missing
until the next restart even after a successful enable through the
dashboard API — Express routes can't be added after listen().
Add a pluginLoader.loadAllPlugins() call before createServer to
mirror what runtime-providing plugins (paperclip etc.) already
assume: enabled plugins are live the moment fusion starts. Failure
of an individual plugin's load doesn't fail startup.
External services (Grafana, Sentry, Slack) call plugin webhooks with
their own per-plugin shared secret — they cannot present the
dashboard's daemon token. Daemon auth was 401'ing those callbacks
before they reached the plugin handler, so even with the route
correctly mounted the secret check inside the plugin never fired.
Add a registry of dynamically-exempt paths populated at server
startup when plugin routes are mounted. Plugin management routes
(/api/plugins, /api/plugins/:id/enable, etc.) stay gated; only the
plugin-defined routes (/api/plugins/:pluginId/<route>) are exempted.
Each plugin handler is responsible for its own secret check (the
telemetry-watcher webhook compares Authorization Bearer against
settings.grafanaWebhookSecret in constant time at the handler).
Phase-1 telemetry-watcher's grafana-webhook handler 401'd because the
dashboard never mounted plugin-supplied routes — getPluginRoutes()
exists on PluginLoader but no caller consumed it. The smoke test was
working around this by injecting incident tasks directly through
/api/tasks; we want the real path to work end-to-end.
Two changes:
1. PluginLoader gains createContextFor(pluginId, { taskStore? }).
Lifecycle hooks still see the loader's bound store (the cwd
project), but REST handlers receive a project-scoped store derived
from the request's projectId so a Grafana webhook addressed to
sase opens tasks in sase even though fusion's loader is bound
to its own cwd. Settings still come from the loader's store at
load time, which is the right thing — settings don't follow the
request.
2. routes.ts iterates pluginLoader.getPluginRoutes() once at server
startup and binds /api/plugins/:pluginId/:routePath to a handler
that resolves project context per request, builds the context via
createContextFor, and forwards to the plugin's route. ApiError +
rethrowAsApiError preserve the dashboard's standard error envelope.
Plugins added after server start still need a restart for routes to
bind; reloadPlugin doesn't currently re-mount Express handlers. That
limitation matches the existing constraint and is out of scope here.
ProjectEngineManager runs one engine per registered project, but the
heartbeat handler in register-agent-runtime-routes.ts only used the
single global heartbeatMonitor passed via ServerOptions. That monitor
is bound to the cwd project's engine, so heartbeat triggers for any
secondary project silently no-op'd: the run id was returned but no
execution actually happened.
Add resolveHeartbeatMonitorFor(scopedStore) that walks
engineManager.getAllEngines() and returns the engine whose working
directory matches the request's scoped store, falling back to the
global monitor when its rootDir matches. Use the resolver at every
heartbeat call site (state-pause stop, /agents/:id/heartbeat,
/agents/:id/runs, /agents/:id/runs/stop).
The dashboard modules used a variable-specifier dynamic import
(`const m = "@fusion/engine"; await import(m)`) to defeat bundler static
analysis. tsup honored that and left the dynamic import in dist/bin.js,
so the published `@runfusion/fusion` package failed at runtime with
"createFnAgent2 is not a function" — `@fusion/engine` isn't on npm and
the silent catch set the binding to undefined. Replaces the trick with
static imports across planning, chat, subtask-breakdown, mission-interview,
agent-generation, ai-refine, roadmap-suggestions, milestone-slice-interview,
and routes. Core can't statically import engine (cycle), so it now exposes
setCreateFnAgent and engine wires itself in at module load. Documents the
pattern in AGENTS.md.
FixesRunfusion/Fusion#9.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- fix(FN-2610): add changeset for health version fix
- test(FN-2610): verify health endpoint returns real package version
- fix(FN-2610): read version from package.json in health endpoint
Update test regexps to match .modal.terminal-modal (two-class selector) and assert min-height: 100dvh on mobile. Also clean up a stale comment in TaskDetailModal.
When the soft keyboard opens on mobile, the overlay's align-items:center was vertically centering the shrunken modal, pushing the header/tabs out of view. Add a rule to switch to align-items:flex-start when --keyboard-overlap is detected.
readStoredAuthProvidersFromDisk() previously returned only the first
successfully-parsed auth file, missing providers that existed only in
fallback locations (e.g. github-copilot in ~/.pi/agent/auth.json when
another provider was in ~/.fusion/agent/auth.json). Now iterates all
candidates and merges entries with first-found-wins priority.
- Persist hidden usage-window IDs in project storage so visibility choices survive reloads
- Add hide/show actions in UsageIndicator rows plus a provider-level control to reveal hidden windows
- Style hidden window rows and usage header actions in PlanningModeModal for clear state and aligned controls
- Expand UsageIndicator and projectStorage tests to cover hide/show behavior and persistence callbacks
- Add a GitHub Copilot usage fetcher to usage collection flow
- Include copilot in provider aggregation so totals include Copilot consumption
- Map the copilot provider to the correct icon key in UsageIndicator
- Expand usage tests to cover Copilot provider parsing and aggregation behavior
- Add register-file-workspace-routes.ts with task file, workspace discovery, file operation, and markdown/search endpoints
- Mount the new file workspace registrar from routes.ts using shared route context and injected helpers
- Remove file/workspace route implementations from routes.ts and register-task-workflow-routes.ts to keep domain boundaries clear
- Update routes/README.md with registrar responsibilities and ordering constraints for file wildcard routes
- Render provider icons on TaskCard model metadata with token-based sizing and spacing
- Add provider icon display in TaskDetailModal for executor, validator, and planning model rows
- Update dashboard styling with reusable provider icon classes and layout tweaks in component CSS
- Expand TaskCard tests to cover provider icon rendering and fallback behavior
- Add a dedicated Bedrock SVG provider icon and provider token styling
- Wire Bedrock provider aliases so provider detection resolves to the new icon key
- Update usage indicator mapping so Bedrock usage providers render the correct icon
- Expand ProviderIcon tests to cover Bedrock icon rendering and alias coverage
- Add dedicated xAI and Opencode SVG icon components in ProviderIcon with provider config entries
- Map grok to the xAI icon and add opencode color token for consistent themed rendering
- Extend UsageIndicator provider normalization to resolve xai/grok and opencode icon keys
- Add ProviderIcon tests for xai, grok alias behavior, and opencode rendering/color normalization
Builds now emit version.json + a __BUILD_VERSION__ define. The client
re-checks the remote version on visibilitychange/focus and reloads on
mismatch, so a backgrounded tab doesn't hit a 404'd hashed chunk and
surface "'text/html' is not a valid JavaScript MIME type" when opening
Settings. ErrorBoundary catches stale-chunk errors as a safety net.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>