Skip to content

Commit fb41a2e

Browse files
authored
refactor: fixed policy approver to be more responsive (#34)
1 parent 88eb9a0 commit fb41a2e

File tree

2 files changed

+625
-44
lines changed

2 files changed

+625
-44
lines changed

sandboxes/nemoclaw/nemoclaw-ui-extension/extension/index.ts

Lines changed: 277 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,46 @@ const STABLE_CONNECTION_WINDOW_MS = 10_000;
2626
const STABLE_CONNECTION_TIMEOUT_MS = 45_000;
2727
const PAIRING_RELOAD_FLAG = "nemoclaw:pairing-bootstrap-reloaded";
2828
const FORCED_RELOAD_DELAY_MS = 1_000;
29+
const PAIRING_STATUS_POLL_MS = 500;
30+
const PAIRING_REARM_INTERVAL_MS = 4_000;
31+
const OVERLAY_SHOW_DELAY_MS = 400;
32+
const PAIRING_BOOTSTRAPPED_FLAG = "nemoclaw:pairing-bootstrap-complete";
33+
const POST_READY_SETTLE_MS = 750;
34+
const WARM_START_CONNECTION_WINDOW_MS = 500;
35+
const WARM_START_TIMEOUT_MS = 2_500;
36+
const READINESS_HANDLED = Symbol("pairing-bootstrap-readiness-handled");
37+
38+
interface PairingBootstrapState {
39+
status?: string;
40+
approvedCount?: number;
41+
active?: boolean;
42+
lastApprovalDeviceId?: string;
43+
lastError?: string;
44+
sawBrowserPaired?: boolean;
45+
}
46+
47+
const PAIRING_STATUS_PRIORITY: Record<string, number> = {
48+
idle: 0,
49+
armed: 1,
50+
pending: 2,
51+
approving: 3,
52+
"approved-pending-settle": 4,
53+
"paired-other-device": 5,
54+
paired: 6,
55+
timeout: 7,
56+
error: 7,
57+
};
58+
59+
function isPairingTerminal(state: PairingBootstrapState | null): boolean {
60+
if (!state) return false;
61+
if (state.active) return false;
62+
return state.status === "paired" || state.status === "timeout" || state.status === "error";
63+
}
64+
65+
function isPairingRecoveryEligible(state: PairingBootstrapState | null): boolean {
66+
if (!state) return false;
67+
return state.status === "paired";
68+
}
2969

3070
function inject(): boolean {
3171
const hasButton = injectButton();
@@ -71,6 +111,7 @@ function setConnectOverlayText(text: string): void {
71111
}
72112

73113
function revealApp(): void {
114+
markPairingBootstrapped();
74115
document.body.setAttribute("data-nemoclaw-ready", "");
75116
const overlay = document.querySelector(".nemoclaw-connect-overlay");
76117
if (overlay) {
@@ -80,82 +121,274 @@ function revealApp(): void {
80121
startDenialWatcher();
81122
}
82123

83-
function shouldForcePairingReload(): boolean {
124+
function shouldAllowRecoveryReload(): boolean {
84125
try {
85126
return sessionStorage.getItem(PAIRING_RELOAD_FLAG) !== "1";
86127
} catch {
87128
return true;
88129
}
89130
}
90131

91-
function markPairingReloadComplete(): void {
132+
function isPairingBootstrapped(): boolean {
92133
try {
93-
sessionStorage.setItem(PAIRING_RELOAD_FLAG, "1");
134+
return sessionStorage.getItem(PAIRING_BOOTSTRAPPED_FLAG) === "1";
135+
} catch {
136+
return false;
137+
}
138+
}
139+
140+
function markPairingBootstrapped(): void {
141+
try {
142+
sessionStorage.setItem(PAIRING_BOOTSTRAPPED_FLAG, "1");
94143
} catch {
95144
// ignore storage failures
96145
}
97146
}
98147

99-
function clearPairingReloadFlag(): void {
148+
function markRecoveryReloadUsed(): void {
100149
try {
101-
sessionStorage.removeItem(PAIRING_RELOAD_FLAG);
150+
sessionStorage.setItem(PAIRING_RELOAD_FLAG, "1");
102151
} catch {
103152
// ignore storage failures
104153
}
105154
}
106155

107-
function forcePairingReload(reason: string, overlayText: string): void {
108-
console.info(`[NeMoClaw] pairing bootstrap: forcing one-time reload (${reason})`);
109-
markPairingReloadComplete();
110-
setConnectOverlayText(overlayText);
111-
window.setTimeout(() => window.location.reload(), FORCED_RELOAD_DELAY_MS);
156+
async function fetchPairingBootstrapState(method: "GET" | "POST"): Promise<PairingBootstrapState | null> {
157+
try {
158+
const res = await fetch("/api/pairing-bootstrap", { method });
159+
if (!res.ok) return null;
160+
return (await res.json()) as PairingBootstrapState;
161+
} catch {
162+
return null;
163+
}
164+
}
165+
166+
function getOverlayTextForPairingState(state: PairingBootstrapState | null): string | null {
167+
switch (state?.status) {
168+
case "armed":
169+
return "Preparing device pairing bootstrap...";
170+
case "pending":
171+
return "Waiting for device pairing request...";
172+
case "approving":
173+
return "Approving device pairing...";
174+
case "approved-pending-settle":
175+
return "Device pairing approved. Waiting for dashboard device to finish pairing...";
176+
case "paired-other-device":
177+
return "Pairing another device. Waiting for browser dashboard pairing...";
178+
case "paired":
179+
return "Device paired. Finalizing dashboard...";
180+
case "approved":
181+
return "Device pairing approved. Waiting for browser dashboard pairing...";
182+
case "timeout":
183+
return "Pairing bootstrap timed out. Opening dashboard...";
184+
case "error":
185+
return "Pairing bootstrap hit an error. Opening dashboard...";
186+
default:
187+
return null;
188+
}
112189
}
113190

114191
function bootstrap() {
115192
console.info("[NeMoClaw] pairing bootstrap: start");
116-
showConnectOverlay();
117193

118-
const finalizeConnectedState = async () => {
119-
setConnectOverlayText("Device pairing approved. Finalizing dashboard...");
120-
console.info("[NeMoClaw] pairing bootstrap: reconnect detected");
121-
if (shouldForcePairingReload()) {
122-
forcePairingReload("post-reconnect", "Device pairing approved. Reloading dashboard...");
123-
return;
194+
let pairingPollTimer = 0;
195+
let overlayTimer = 0;
196+
let stopped = false;
197+
let dashboardStable = false;
198+
let latestPairingState: PairingBootstrapState | null = null;
199+
let lastPairingStartAt = 0;
200+
let overlayVisible = false;
201+
let overlayPriority = -1;
202+
203+
const stopPairingPoll = () => {
204+
stopped = true;
205+
if (pairingPollTimer) window.clearTimeout(pairingPollTimer);
206+
if (overlayTimer) window.clearTimeout(overlayTimer);
207+
};
208+
209+
const ensureOverlayVisible = () => {
210+
if (overlayVisible) return;
211+
overlayVisible = true;
212+
showConnectOverlay();
213+
};
214+
215+
const setMonotonicOverlayText = (text: string | null, status?: string) => {
216+
if (!text) return;
217+
const nextPriority = PAIRING_STATUS_PRIORITY[status || ""] ?? overlayPriority;
218+
if (nextPriority < overlayPriority) return;
219+
overlayPriority = nextPriority;
220+
setConnectOverlayText(text);
221+
};
222+
223+
const scheduleOverlay = () => {
224+
if (overlayVisible || overlayTimer) return;
225+
overlayTimer = window.setTimeout(() => {
226+
overlayTimer = 0;
227+
ensureOverlayVisible();
228+
}, OVERLAY_SHOW_DELAY_MS);
229+
};
230+
231+
const pollPairingState = async () => {
232+
if (stopped) return null;
233+
const state = await fetchPairingBootstrapState("GET");
234+
latestPairingState = state;
235+
const text = getOverlayTextForPairingState(state);
236+
setMonotonicOverlayText(text, state?.status);
237+
238+
if (
239+
!stopped &&
240+
!dashboardStable &&
241+
state &&
242+
!state.active &&
243+
!isPairingTerminal(state) &&
244+
Date.now() - lastPairingStartAt >= PAIRING_REARM_INTERVAL_MS
245+
) {
246+
const rearmed = await fetchPairingBootstrapState("POST");
247+
if (rearmed) {
248+
latestPairingState = rearmed;
249+
lastPairingStartAt = Date.now();
250+
const rearmedText = getOverlayTextForPairingState(rearmed);
251+
setMonotonicOverlayText(rearmedText, rearmed.status);
252+
}
253+
}
254+
255+
pairingPollTimer = window.setTimeout(pollPairingState, PAIRING_STATUS_POLL_MS);
256+
return state;
257+
};
258+
259+
const waitForDashboardReadiness = async (timeoutMs: number, overlayText: string) => {
260+
ensureOverlayVisible();
261+
setConnectOverlayText(overlayText);
262+
await waitForStableConnection(STABLE_CONNECTION_WINDOW_MS, timeoutMs);
263+
};
264+
265+
const handlePairingTerminalWithoutStableConnection = async (reason: string) => {
266+
const state = latestPairingState || (await fetchPairingBootstrapState("GET"));
267+
const status = state?.status || "unknown";
268+
if (isPairingRecoveryEligible(state) && shouldAllowRecoveryReload()) {
269+
console.warn(`[NeMoClaw] pairing bootstrap: ${reason}; pairing=${status}; forcing one recovery reload`);
270+
stopPairingPoll();
271+
markRecoveryReloadUsed();
272+
setConnectOverlayText("Pairing succeeded. Recovering dashboard...");
273+
window.setTimeout(() => window.location.reload(), 750);
274+
return true;
124275
}
125-
setConnectOverlayText("Device pairing approved. Verifying dashboard health...");
126-
try {
127-
console.info("[NeMoClaw] pairing bootstrap: waiting for stable post-reload connection");
128-
await waitForStableConnection(
129-
STABLE_CONNECTION_WINDOW_MS,
130-
STABLE_CONNECTION_TIMEOUT_MS,
131-
);
132-
} catch {
133-
console.warn("[NeMoClaw] pairing bootstrap: stable post-reload connection check timed out; delaying reveal");
134-
await new Promise((resolve) => setTimeout(resolve, POST_PAIRING_SETTLE_DELAY_MS));
276+
if (isPairingTerminal(state)) {
277+
console.warn(`[NeMoClaw] pairing bootstrap: ${reason}; pairing=${status}; revealing app without further delay`);
278+
stopPairingPoll();
279+
revealApp();
280+
return true;
135281
}
136-
console.info("[NeMoClaw] pairing bootstrap: reveal app");
137-
clearPairingReloadFlag();
138-
revealApp();
282+
return false;
139283
};
140284

141-
waitForReconnect(INITIAL_CONNECT_TIMEOUT_MS)
142-
.then(finalizeConnectedState)
143-
.catch(async () => {
144-
console.warn("[NeMoClaw] pairing bootstrap: initial reconnect timed out; extending wait");
145-
if (shouldForcePairingReload()) {
146-
forcePairingReload("initial-timeout", "Pairing is still settling. Reloading dashboard...");
147-
return;
285+
const runReadinessFlow = () => {
286+
waitForDashboardReadiness(
287+
INITIAL_CONNECT_TIMEOUT_MS,
288+
"Auto-approving device pairing. Hang tight...",
289+
)
290+
.catch(async () => {
291+
console.warn("[NeMoClaw] pairing bootstrap: initial dashboard readiness check timed out; extending wait");
292+
if (await handlePairingTerminalWithoutStableConnection("initial readiness timed out")) {
293+
throw READINESS_HANDLED;
294+
}
295+
return waitForDashboardReadiness(
296+
EXTENDED_CONNECT_TIMEOUT_MS,
297+
"Still waiting for device pairing approval...",
298+
);
299+
})
300+
.then(async () => {
301+
await new Promise((resolve) => window.setTimeout(resolve, POST_READY_SETTLE_MS));
302+
const settledState = await fetchPairingBootstrapState("GET");
303+
if (settledState) latestPairingState = settledState;
304+
305+
dashboardStable = true;
306+
console.info("[NeMoClaw] pairing bootstrap: reveal app");
307+
stopPairingPoll();
308+
setConnectOverlayText("Device pairing approved. Opening dashboard...");
309+
revealApp();
310+
})
311+
.catch(async (err: unknown) => {
312+
if (err === READINESS_HANDLED) return;
313+
if (stopped) return;
314+
if (dashboardStable) return;
315+
if (await handlePairingTerminalWithoutStableConnection("extended readiness timed out")) {
316+
return;
317+
}
318+
const state = latestPairingState || (await fetchPairingBootstrapState("GET"));
319+
const status = state?.status || "unknown";
320+
console.warn(`[NeMoClaw] pairing bootstrap: readiness timed out; revealing app anyway (status=${status})`);
321+
stopPairingPoll();
322+
revealApp();
323+
});
324+
};
325+
326+
void (async () => {
327+
const initialState = await fetchPairingBootstrapState("GET");
328+
latestPairingState = initialState;
329+
330+
if (initialState && !initialState.active && isPairingTerminal(initialState)) {
331+
const shouldWarmStart = isPairingBootstrapped() || initialState.status === "paired";
332+
if (shouldWarmStart) {
333+
try {
334+
await waitForStableConnection(WARM_START_CONNECTION_WINDOW_MS, WARM_START_TIMEOUT_MS);
335+
console.info("[NeMoClaw] pairing bootstrap: warm start succeeded");
336+
stopPairingPoll();
337+
revealApp();
338+
return;
339+
} catch {
340+
// Fall through to normal bootstrap flow.
341+
}
148342
}
149-
setConnectOverlayText("Still waiting for device pairing approval...");
343+
}
344+
345+
if (initialState === null) {
346+
// Endpoint missing or failed — fall back to reconnect-only flow.
347+
showConnectOverlay();
150348
try {
151-
await waitForReconnect(EXTENDED_CONNECT_TIMEOUT_MS);
152-
await finalizeConnectedState();
349+
await waitForReconnect(INITIAL_CONNECT_TIMEOUT_MS);
350+
setConnectOverlayText("Device pairing approved. Finalizing dashboard...");
351+
if (shouldAllowRecoveryReload()) {
352+
markRecoveryReloadUsed();
353+
setConnectOverlayText("Device pairing approved. Reloading dashboard...");
354+
window.setTimeout(() => window.location.reload(), FORCED_RELOAD_DELAY_MS);
355+
return;
356+
}
357+
await waitForStableConnection(STABLE_CONNECTION_WINDOW_MS, STABLE_CONNECTION_TIMEOUT_MS);
153358
} catch {
154-
console.warn("[NeMoClaw] pairing bootstrap: extended reconnect timed out; revealing app anyway");
155-
clearPairingReloadFlag();
156-
revealApp();
359+
setConnectOverlayText("Still waiting for device pairing approval...");
360+
try {
361+
await waitForReconnect(EXTENDED_CONNECT_TIMEOUT_MS);
362+
await waitForStableConnection(STABLE_CONNECTION_WINDOW_MS, STABLE_CONNECTION_TIMEOUT_MS);
363+
} catch {
364+
// reveal anyway
365+
}
157366
}
158-
});
367+
revealApp();
368+
return;
369+
}
370+
371+
scheduleOverlay();
372+
const initialText = getOverlayTextForPairingState(initialState);
373+
if (initialText) {
374+
ensureOverlayVisible();
375+
setMonotonicOverlayText(initialText, initialState?.status);
376+
}
377+
378+
if (!initialState.active && !isPairingTerminal(initialState)) {
379+
ensureOverlayVisible();
380+
const started = await fetchPairingBootstrapState("POST");
381+
if (started) {
382+
latestPairingState = started;
383+
lastPairingStartAt = Date.now();
384+
const startedText = getOverlayTextForPairingState(started);
385+
setMonotonicOverlayText(startedText, started.status);
386+
}
387+
}
388+
389+
await pollPairingState();
390+
runReadinessFlow();
391+
})();
159392

160393
const keysIngested = ingestKeysFromUrl();
161394

0 commit comments

Comments
 (0)