Skip to content

gateway: a single transient poll error permanently and silently replaces live data with mock data #2

Description

@space-shell

Summary

A single transient poll error permanently and silently replaces live gateway data with mock data. The switch destroys the real graph and nothing in the UI indicates the data is fabricated.

Root cause

  • poll() emits a connection: error event on any single failed poll (real.ts:229-231).
  • FallbackGateway.forward (real.ts:253-262) switches to MockPaseoGateway on the first such error while the primary is active; switchToFallback (:278-284) has no return path.
  • The mock synchronously emits a snapshot on start (mock.ts:225-226); pruneOrphans (project.ts:184-207) then deletes every gateway-origin node absent from the mock ids, i.e. the entire real graph.
  • run() never retries a failed initial connect (real.ts:164-167).
  • No consumer renders connection events (project.ts:286-287 is a no-op), so provenance is invisible.

Falling back to mock on the GitHub Pages origin is documented intent (real.ts:236-237, App.tsx:37-38). The defect is that transient and fatal errors share one signal, the switch is irreversible, and the substitution is undetectable.

Failure scenario

Daemon restart or laptop sleep spanning one 4-second poll window: real nodes are deleted, the scripted demo scenario renders in their place, and the session continues on fabricated data until a reload.

Proposed solution

Distinguish connect-phase from poll-phase errors (or require N consecutive poll failures before emitting a fatal error); retry the initial connect with capped backoff; render connection state and data provenance (live vs mock) somewhere visible. Re-promotion to the primary can come later; the silent substitution is the part to fix first.

References

private forward = (event: GatewayEvent): void => {
for (const listener of this.listeners) listener(event);
if (
event.kind === "connection" &&
event.state === "error" &&
this.active === this.primary
) {
this.switchToFallback();
}
};

} catch (err) {
this.emit({ kind: "connection", state: "error", detail: String(err) });
}

function pruneOrphans(store: GraphStore, snapshot: GatewaySnapshot): number {
const liveIds = new Set<string>([
...snapshot.workspaces.map((w) => w.id),
...snapshot.agents.map((a) => a.id),
]);
const state = store.getState();
let removed = 0;
for (const node of Object.values(state.nodes)) {
if (node.origin !== "gateway" || node.kind === "server" || node.kind === "project") continue;
if (node.externalId !== null && !liveIds.has(node.externalId)) {
store.getState().removeNode(node.id);
removed += 1;
}
}
// projects with no remaining workspaces are removed too
const after = store.getState();
const projectIds = new Set(snapshot.workspaces.map((w) => w.projectId));
for (const node of Object.values(after.nodes)) {
if (node.origin === "gateway" && node.kind === "project" && !projectIds.has(node.externalId ?? "")) {
store.getState().removeNode(node.id);
removed += 1;
}
}
return removed;

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions