Keep Errored Sources Visible
What happens today
config-sync (apps/local/src/server/config-sync.ts) reads
executor.jsonc and calls the plugin's addSource / addSpec for
each entry. If one fails (auth, network, unparseable spec), the error
is logged and the rest keep loading:
[config-sync] Failed to load source "mcp_axiom_co": MCP discovery failed:
Failed connecting to MCP server: Failed connecting via sse:
SSE error: Non-200 status code (401)
[config-sync] 3/4 source(s) synced
The row never reaches the DB. executor.jsonc still has it, so next
boot retries — idempotent, but from the UI's perspective the source
has silently vanished.
The gap
The user sees 3 sources in the UI. They have no way to know a 4th existed, was tried, and failed. The only record of the failure is stderr. Reasonable people end up confused — "did it delete my source?"
What we want
The source stays visible after a failed add, marked as errored with a
human-readable reason. The UI shows it with a warning badge; a retry
button calls refresh or re-runs the config-sync entry for just that
source.
Sketch
Schema — extend the source table:
source: {
fields: {
id: { type: "string", required: true },
scope_id: { type: "string", required: true, index: true },
// ...existing...
status: { type: "string", required: true, defaultValue: "healthy" }, // "healthy" | "error"
last_error: { type: "string" }, // message
last_error_at: { type: "date" }, // for UI "last tried N minutes ago"
},
}
config-sync — on failure, still write a row:
Effect.catchAll((e) => {
const message = e instanceof Error ? e.message : String(e);
return executor.sources.recordError({ source, message }).pipe(Effect.asVoid);
});
executor.sources.recordError is a new core method that writes a
source row with status="error" and last_error. Plugin-specific
data (spec JSON, endpoint URL, headers) is still pulled from the
config entry, so the row is mostly populated — just no tools
attached because discovery never ran.
UI — sources list badges errored entries, shows last_error on
hover/click, offers "Retry" which calls executor.sources.refresh(id).
Interaction with removes
An errored source is still user-visible, so removeSource should work
on it (user can delete the jsonc entry and the DB row together). Not
a special case — current removeSource already works regardless of
status.
Scope
Not for the scope-refactor PR. Separate change:
- Schema migration adds two columns.
executor.sources.recordErroron the core SDK.- Wire up in config-sync.
- UI badge + retry button in the sources list.
Doable in a day. Worth doing because silent source dropping is the single most confusing UX we have today.