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; |
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 aconnection: errorevent on any single failed poll (real.ts:229-231).FallbackGateway.forward(real.ts:253-262) switches toMockPaseoGatewayon the first such error while the primary is active;switchToFallback(:278-284) has no return path.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).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
unlimigent/src/gateway/real.ts
Lines 253 to 262 in eb343c3
unlimigent/src/gateway/real.ts
Lines 229 to 231 in eb343c3
unlimigent/src/gateway/project.ts
Lines 184 to 207 in eb343c3