openclaw-navigator 5.7.3 → 5.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/cli.mjs +67 -19
  2. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -1193,10 +1193,28 @@ function handleRequest(req, res) {
1193
1193
 
1194
1194
  // 3. Background: relay to BFF (port 4000) /api/chat for AI processing
1195
1195
  // Format: OpenAI-compatible chat completions with message history
1196
- const chatHistory = session.messages.map((m) => ({
1197
- role: m.role,
1198
- content: m.content,
1199
- }));
1196
+ // Sanitize: BFF expects alternating user/assistant roles, starting with user.
1197
+ // Drop orphaned assistant messages at the start (from web UI SSE tap).
1198
+ let chatHistory = session.messages
1199
+ .map((m) => ({ role: m.role, content: m.content }))
1200
+ .filter((m) => m.role === "user" || m.role === "assistant" || m.role === "system");
1201
+ // Ensure first non-system message is 'user' — drop leading assistant messages
1202
+ while (chatHistory.length > 0 && chatHistory[0].role === "assistant") {
1203
+ chatHistory.shift();
1204
+ }
1205
+ // Collapse consecutive same-role messages (merge them)
1206
+ chatHistory = chatHistory.reduce((acc, msg) => {
1207
+ if (acc.length > 0 && acc[acc.length - 1].role === msg.role) {
1208
+ acc[acc.length - 1].content += "\n" + msg.content;
1209
+ } else {
1210
+ acc.push({ ...msg });
1211
+ }
1212
+ return acc;
1213
+ }, []);
1214
+ // Must have at least one user message
1215
+ if (chatHistory.length === 0 || chatHistory[0].role !== "user") {
1216
+ chatHistory.unshift({ role: "user", content: message });
1217
+ }
1200
1218
  const proxyBody = JSON.stringify({
1201
1219
  messages: chatHistory,
1202
1220
  stream: true,
@@ -1217,6 +1235,7 @@ function handleRequest(req, res) {
1217
1235
  };
1218
1236
 
1219
1237
  console.log(` ${DIM}→ Relaying to BFF /api/chat (port ${ocUIPort}) with ${chatHistory.length} messages${relayCookie ? " +cookie" : " NO-COOKIE"}${RESET}`);
1238
+ console.log(` ${DIM} Body: ${proxyBody.substring(0, 300)}${proxyBody.length > 300 ? "..." : ""}${RESET}`);
1220
1239
 
1221
1240
  // Helper: send the relay request (with optional retry after login)
1222
1241
  function sendBFFRelay(opts, body, retryCount = 0) {
@@ -1341,7 +1360,15 @@ function handleRequest(req, res) {
1341
1360
  if (!isSSE && sseBuffer) {
1342
1361
  try {
1343
1362
  const jsonBody = JSON.parse(sseBuffer);
1344
- fullText = jsonBody.choices?.[0]?.message?.content || jsonBody.response || jsonBody.message || jsonBody.text || "";
1363
+ // Check for error response (BFF returns 400/500 with error details)
1364
+ if (proxyRes.statusCode >= 400) {
1365
+ const errMsg = jsonBody.error?.message || jsonBody.error || jsonBody.message || JSON.stringify(jsonBody);
1366
+ console.log(` ${RED}✗${RESET} BFF error ${proxyRes.statusCode}: ${String(errMsg).substring(0, 300)}`);
1367
+ // Send error to Navigator so user sees it
1368
+ fullText = `⚠️ Error from AI: ${String(errMsg).substring(0, 200)}`;
1369
+ } else {
1370
+ fullText = jsonBody.choices?.[0]?.message?.content || jsonBody.response || jsonBody.message || jsonBody.text || "";
1371
+ }
1345
1372
  } catch {
1346
1373
  console.log(` ${DIM}BFF returned non-JSON (${sseBuffer.length} bytes): ${sseBuffer.substring(0, 200)}${RESET}`);
1347
1374
  }
@@ -1359,7 +1386,7 @@ function handleRequest(req, res) {
1359
1386
  });
1360
1387
  console.log(` ${GREEN}✓${RESET} AI response (${fullText.length} chars): ${fullText.substring(0, 80)}...`);
1361
1388
  } else {
1362
- console.log(` ${DIM}BFF returned no content${RESET}`);
1389
+ console.log(` ${DIM}BFF returned no content (status ${proxyRes.statusCode})${RESET}`);
1363
1390
  }
1364
1391
  });
1365
1392
 
@@ -1640,16 +1667,15 @@ function handleRequest(req, res) {
1640
1667
  res.writeHead(200, headers);
1641
1668
  res.end(JSON.stringify(result));
1642
1669
 
1643
- // Store in bridge session + broadcast via WebSocket
1670
+ // Broadcast via WebSocket for live updates (but DON'T store in
1671
+ // bridge chat session — web UI manages its own conversation state
1672
+ // separately from sidepane chat. Mixing them causes 400 errors from
1673
+ // the BFF due to malformed message history.)
1644
1674
  if (fullText) {
1645
- const session = getChatSession("main");
1646
- session.messages.push({ role: "assistant", content: fullText, timestamp: Date.now() });
1647
-
1648
1675
  broadcastToWS({
1649
- type: "chat.final",
1676
+ type: "chat.webui",
1650
1677
  text: fullText,
1651
1678
  content: fullText,
1652
- sessionKey: "main",
1653
1679
  role: "assistant",
1654
1680
  timestamp: Date.now(),
1655
1681
  });
@@ -1703,6 +1729,23 @@ async function registerWithRelay(code, url, token, name) {
1703
1729
  });
1704
1730
  clearTimeout(timeout);
1705
1731
  const data = await res.json();
1732
+
1733
+ // Also register with token as the lookup key — so Navigator can
1734
+ // resolve by saved token even after the pairing code rotates.
1735
+ // Uses first 8 chars of token as a "token code" (avoids relay key limits).
1736
+ const tokenCode = `T${token.substring(0, 7)}`;
1737
+ try {
1738
+ const c2 = new AbortController();
1739
+ const t2 = setTimeout(() => c2.abort(), 5000);
1740
+ await fetch(`${RELAY_URL}/register`, {
1741
+ method: "POST",
1742
+ headers: { "Content-Type": "application/json" },
1743
+ body: JSON.stringify({ code: tokenCode, url, token, name }),
1744
+ signal: c2.signal,
1745
+ });
1746
+ clearTimeout(t2);
1747
+ } catch { /* non-critical */ }
1748
+
1706
1749
  return data.ok === true;
1707
1750
  } catch {
1708
1751
  return false;
@@ -1927,25 +1970,30 @@ module.exports = {
1927
1970
  await killPort(ocUIPort);
1928
1971
  startWebUI();
1929
1972
 
1930
- // ── Step 2: Persistent identity ─────────────────────────────────────
1931
- // Reuse the same pairing code + token across restarts so Navigator
1932
- // doesn't need to re-pair. The code resolves to the NEW tunnel URL
1933
- // via the relay, so everything reconnects automatically.
1973
+ // ── Step 2: Identity persistent token, rotating code ──────────────
1974
+ // Token is persistent: Navigator stores it after first pairing and uses
1975
+ // it for all future requests. This is the real auth credential.
1976
+ // Pairing code rotates every startup: it's a one-time handshake to
1977
+ // exchange the token. A new code every time means nobody can reuse
1978
+ // an old code to hijack the connection.
1934
1979
  const displayName = hostname().replace(/\.local$/, "");
1935
1980
  let token;
1936
1981
 
1937
1982
  const savedIdentity = freshIdentity ? null : loadBridgeIdentity();
1938
1983
  if (savedIdentity) {
1939
- pairingCode = savedIdentity.pairingCode;
1984
+ // Keep the token (Navigator already has it) but rotate the code
1940
1985
  token = savedIdentity.token;
1941
1986
  validTokens.add(token);
1942
- ok(`Restored pairing code: ${BOLD}${GREEN}${pairingCode}${RESET} (same as last session)`);
1987
+ pairingCode = generatePairingCode(); // fresh code every startup
1988
+ saveBridgeIdentity(pairingCode, token, displayName);
1989
+ ok(`Token restored (Navigator will auto-reconnect)`);
1990
+ ok(`New pairing code: ${BOLD}${GREEN}${pairingCode}${RESET} (rotated for security)`);
1943
1991
  } else {
1944
1992
  token = randomUUID().replace(/-/g, "");
1945
1993
  validTokens.add(token);
1946
1994
  pairingCode = generatePairingCode();
1947
1995
  saveBridgeIdentity(pairingCode, token, displayName);
1948
- ok(`New pairing code generated: ${BOLD}${GREEN}${pairingCode}${RESET}`);
1996
+ ok(`New pairing code: ${BOLD}${GREEN}${pairingCode}${RESET}`);
1949
1997
  }
1950
1998
 
1951
1999
  let gatewayURL = `http://localhost:${port}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-navigator",
3
- "version": "5.7.3",
3
+ "version": "5.7.5",
4
4
  "description": "One-command bridge + tunnel for the Navigator browser — works on any machine, any OS",
5
5
  "keywords": [
6
6
  "browser",