openzoo 0.50.96 → 0.50.98

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.
@@ -33,20 +33,24 @@ import os from 'node:os';
33
33
  import path from 'node:path';
34
34
  import zlib from 'node:zlib';
35
35
  import { execFileSync, execFile } from 'node:child_process';
36
+ import { createRequire } from 'node:module';
36
37
  import { promisify } from 'node:util';
37
38
  import tls from 'node:tls';
38
- import { randomUUID } from 'node:crypto';
39
- import { encodeAvailableModels, encodeForMethod, encodeEnsureSandBox, encodeGetGrokBotSendStatus, decodeProtoFields, unwrapConnect } from './cursorapi.js';
39
+ import { randomUUID, generateKeyPairSync, createPrivateKey, sign as cryptoSign } from 'node:crypto';
40
+ import { encodeAvailableModels, encodeForMethod, encodeEnsureSandBox, encodeGetGrokBotSendStatus, decodeProtoFields, unwrapConnect, fakeCursorAuthReply } from './cursorapi.js';
40
41
  import {
41
42
  accountPodPath, accountAgentsPath, rosterForAccount, rosterForEvent,
42
43
  readHouseRoster, houseAgentsPath, shapeAgent, agentBrief, briefFromName,
43
44
  preferNamedAgent, looksLikeAgentId,
45
+ grokroomAgentId, grokroomMemberId, grokroomShareUrl, grokroomFromHandle, grokroomAddrFromShare,
46
+ agentShareUrl, groupShareState, attachOnchainRoom, bindOnchainRoom,
47
+ isGrokRoomAgent, isGrokRoomMember,
44
48
  readWakeups, writeWakeups, shapeWakeup, parseWakeupEvery, wantsWakeupCron,
45
49
  DEFAULT_WAKEUP_PROMPT, addDeletedIds, filterDeleted,
46
50
  } from './grokbotAccount.js';
47
51
  import { spendChipLabel, formatSpendFooter, mergeTurnProof } from './spendProof.js';
48
52
  import { spendLinesOnly, writeSpendHud } from './ozSpendChip.js';
49
- import { zooModelIds, zooModelRow, mediaKindOf } from './models.js';
53
+ import { zooModelIds, zooModelRow, mediaKindOf, resolveModel } from './models.js';
50
54
  import { prefixVisitorRichText } from './grokbotweb.js';
51
55
  import {
52
56
  ingestUpload, lookupUpload, readUploadChunk, readUploadImage, readUploadText,
@@ -66,11 +70,36 @@ const CURSOR_HOSTS = ['api2.cursor.sh', 'api3.cursor.sh', 'api4.cursor.sh', 'rep
66
70
  // handshake before ALPN — logged as `HANDSHAKE FAILED ECONNRESET alpn=?`, with
67
71
  // no request ever reaching us. Same cert, one more pair of names.
68
72
  const ANTHROPIC_TLS_HOSTS = ['api.anthropic.com', 'api-staging.anthropic.com'];
69
- const TLS_HOSTS = [...CURSOR_HOSTS, ...ANTHROPIC_TLS_HOSTS];
73
+ const TLS_DNS = [...CURSOR_HOSTS, ...ANTHROPIC_TLS_HOSTS, 'localhost'];
74
+ const TLS_IPS = ['127.0.0.1'];
75
+ const TLS_HOSTS = [...TLS_DNS, ...TLS_IPS];
70
76
 
71
- /** Generate (once) a self-signed cert covering every host we impersonate. openssl
72
- * is on every mac/linux; this is the only external tool and it is not a trust op. */
73
- export function ensureCert(log = () => {}) {
77
+ /** Node-only self-signed PEM. Windows has no openssl in PATH (measured 2026-09-03).
78
+ * selfsigned@5 `generate` is async. */
79
+ export async function mintTlsPems() {
80
+ const selfsigned = createRequire(import.meta.url)('selfsigned');
81
+ const pems = await selfsigned.generate(
82
+ [{ name: 'commonName', value: 'api2.cursor.sh' }],
83
+ {
84
+ keySize: 2048,
85
+ days: 3650,
86
+ algorithm: 'sha256',
87
+ extensions: [{
88
+ name: 'subjectAltName',
89
+ altNames: [
90
+ ...TLS_DNS.map((value) => ({ type: 2, value })),
91
+ ...TLS_IPS.map((ip) => ({ type: 7, ip })),
92
+ ],
93
+ }],
94
+ },
95
+ );
96
+ if (!pems?.cert || !pems?.private) throw new Error('selfsigned returned empty pem');
97
+ return { cert: pems.cert, key: pems.private };
98
+ }
99
+
100
+ /** Generate (once) a self-signed cert covering every host we impersonate.
101
+ * Mint with the `selfsigned` package (no openssl). openssl is a fallback only. */
102
+ export async function ensureCert(log = () => {}) {
74
103
  const cert = path.join(TLS_DIR, 'cert.pem');
75
104
  const key = path.join(TLS_DIR, 'key.pem');
76
105
  // STALE-CERT INVALIDATION. A plain existence check meant anyone who had ever
@@ -85,12 +114,19 @@ export function ensureCert(log = () => {}) {
85
114
  log('cursor-tls: cert predates the current host list — re-minting');
86
115
  } catch { /* make it */ }
87
116
  fs.mkdirSync(TLS_DIR, { recursive: true });
88
- const san = `subjectAltName=${TLS_HOSTS.map((h) => `DNS:${h}`).join(',')}`;
89
- execFileSync('openssl', [
90
- 'req', '-x509', '-newkey', 'rsa:2048', '-nodes',
91
- '-keyout', key, '-out', cert, '-days', '3650',
92
- '-subj', '/CN=api2.cursor.sh', '-addext', san,
93
- ], { stdio: 'ignore' });
117
+ try {
118
+ const pems = await mintTlsPems();
119
+ fs.writeFileSync(key, pems.key);
120
+ fs.writeFileSync(cert, pems.cert);
121
+ } catch (e) {
122
+ const san = `subjectAltName=${TLS_DNS.map((h) => `DNS:${h}`).join(',')},${TLS_IPS.map((h) => `IP:${h}`).join(',')}`;
123
+ execFileSync('openssl', [
124
+ 'req', '-x509', '-newkey', 'rsa:2048', '-nodes',
125
+ '-keyout', key, '-out', cert, '-days', '3650',
126
+ '-subj', '/CN=api2.cursor.sh', '-addext', san,
127
+ ], { stdio: 'ignore' });
128
+ log(`cursor-tls: openssl fallback (${e.message})`);
129
+ }
94
130
  fs.writeFileSync(stamp, want);
95
131
  log(`cursor-tls: self-signed cert minted at ${TLS_DIR} covering ${TLS_HOSTS.length} hosts (no CA, no trust prompt)`);
96
132
  return { cert, key };
@@ -652,10 +688,10 @@ async function fetchEnsureSandBox(req, body, log) {
652
688
  try {
653
689
  const remote = await podJson('/api/listAgents', {}, log);
654
690
  if (Array.isArray(remote) && remote.length) {
655
- const merged = rosterForEvent(mergeAgentLists(remote), agentActivity);
691
+ const merged = rosterForEvent(mergeAgentLists(remote), agentActivity, { includeHidden: true }); // GROKROOM-HIDDEN-BOTS
656
692
  saveAgents(merged);
657
693
  const active = focusedAgentId || merged[0]?.id;
658
- ssePush('agents', { agents: merged, activeAgentId: active });
694
+ ssePush('agents', { agents: merged.filter((a) => !a.hidden), activeAgentId: active }); // GROKROOM-HIDDEN-BOTS
659
695
  log(`cursor-backend: discovered roster n=${merged.length} account=${realPod.accountId}`);
660
696
  }
661
697
  } catch (e) {
@@ -837,9 +873,9 @@ async function proxyPodHttp(req, res, full, body, log) {
837
873
  try {
838
874
  const parsed = JSON.parse(String(inflateBody(cap.buf, cap.respHeaders)));
839
875
  if (Array.isArray(parsed)) {
840
- const merged = rosterForEvent(mergeAgentLists(parsed), agentActivity);
876
+ const merged = rosterForEvent(mergeAgentLists(parsed), agentActivity, { includeHidden: true }); // GROKROOM-HIDDEN-BOTS
841
877
  saveAgents(merged);
842
- jsonSend(res, merged);
878
+ jsonSend(res, merged.filter((a) => !a.hidden)); // GROKROOM-HIDDEN-BOTS
843
879
  log(`cursor-backend: listAgents 200 merged n=${merged.length} account=${activeAccountId || '?'}`);
844
880
  return true;
845
881
  }
@@ -942,7 +978,7 @@ function sortAgentsByActivity(list) {
942
978
  return ay - ax;
943
979
  });
944
980
  }
945
- function bumpAgent(id, { preview = '', notify = true } = {}) {
981
+ function bumpAgent(id, { preview = '', notify = true, select = true } = {}) {
946
982
  const now = Date.now();
947
983
  const a = agentActivity.get(id) || { unreadCount: 0 };
948
984
  a.updatedAt = now;
@@ -960,7 +996,7 @@ function bumpAgent(id, { preview = '', notify = true } = {}) {
960
996
  if (idx >= 0) list[idx] = agent;
961
997
  else list.unshift(agent);
962
998
  saveAgents(sortAgentsByActivity(list.map(shapeAgent)));
963
- pushCreatedAgent(agent);
999
+ pushCreatedAgent(agent, { select });
964
1000
  }
965
1001
 
966
1002
  /** asar ingestAgentsEvent does Ae.agents.map — `{action:"created"}` has no
@@ -972,7 +1008,7 @@ function pushCreatedAgent(agent, { select = true } = {}) {
972
1008
  const active = focusedAgentId || agent.id;
973
1009
  const list = rosterForEvent(cachedAgentList() || [], agentActivity);
974
1010
  ssePush('agent-upserted', { agent, activeAgentId: active });
975
- ssePush('agents', { agents: list, activeAgentId: active });
1011
+ ssePush('agents', { agents: list.filter((a) => !a.hidden), activeAgentId: active }); // GROKROOM-HIDDEN-BOTS
976
1012
  }
977
1013
 
978
1014
  /** Grok Bot Helper daemon: GET /local-exec/requests is SSE, POST /local-exec/responses
@@ -987,6 +1023,54 @@ function localExecPush(frame) {
987
1023
  try { c.write(chunk); } catch { localExecSse.delete(c); }
988
1024
  }
989
1025
  }
1026
+ /** Helper builds disagree about where the shell output lives: top-level
1027
+ * `stdout` on some, `message: {stdout, exitCode}` or `clientMessage` on
1028
+ * others. Dig it out as TEXT — an object that reaches the canvas paints as
1029
+ * [object Object] (measured: `→ exec "ls" [object Object]` on Linux). */
1030
+ export function normalizeExecFrame(frame) {
1031
+ const out = { stdout: '', stderr: '', exitCode: undefined, message: '', error: '' };
1032
+ const seen = new Set();
1033
+ const pick = (node, keys) => {
1034
+ for (const k of keys) if (typeof node[k] === 'string' && node[k]) return node[k];
1035
+ return '';
1036
+ };
1037
+ const visit = (node, depth) => {
1038
+ if (!node || typeof node !== 'object' || depth > 4 || seen.has(node)) return;
1039
+ seen.add(node);
1040
+ if (!out.stdout) out.stdout = pick(node, ['stdout', 'stdOut', 'output']);
1041
+ if (!out.stderr) out.stderr = pick(node, ['stderr', 'stdErr']);
1042
+ if (!out.message) out.message = pick(node, ['message', 'text', 'content']);
1043
+ if (!out.error) out.error = pick(node, ['error', 'errorMessage']);
1044
+ if (out.exitCode == null) {
1045
+ for (const k of ['exitCode', 'exit_code', 'code']) {
1046
+ if (typeof node[k] === 'number') { out.exitCode = node[k]; break; }
1047
+ }
1048
+ }
1049
+ for (const k of ['message', 'clientMessage', 'serverMessage', 'result', 'data', 'payload', 'body', 'throw']) {
1050
+ visit(node[k], depth + 1);
1051
+ }
1052
+ };
1053
+ visit(frame, 0);
1054
+ if (out.error) out.error = `local-exec: ${out.error}`;
1055
+ if (!out.stdout && !out.stderr && !out.message && !out.error) out.message = toolResultText(frame);
1056
+ return out;
1057
+ }
1058
+
1059
+ /** The daemon streams `output`/`stdout` chunks and only then closes with
1060
+ * `result`/`exit`, and those streaming frames do not always echo requestId.
1061
+ * Keying purely on requestId dropped the body on the floor and waited out the
1062
+ * 45s timeout, so fall back to the sole in-flight request. */
1063
+ function localExecWaiterFor(f) {
1064
+ if (f.requestId && localExecWaiters.has(f.requestId)) {
1065
+ return { id: f.requestId, w: localExecWaiters.get(f.requestId) };
1066
+ }
1067
+ if (!f.requestId && localExecWaiters.size === 1) {
1068
+ const [id, w] = localExecWaiters.entries().next().value;
1069
+ return { id, w };
1070
+ }
1071
+ return null;
1072
+ }
1073
+
990
1074
  function handleLocalExecFrames(frames, log) {
991
1075
  for (const f of frames || []) {
992
1076
  if (!f || typeof f !== 'object') continue;
@@ -994,24 +1078,59 @@ function handleLocalExecFrames(frames, log) {
994
1078
  localExecHello = f;
995
1079
  log(`cursor-backend: local-exec hello computer=${f.computerId || f.label || '?'} root=${f.localRoot || '?'}`);
996
1080
  }
997
- const w = f.requestId && localExecWaiters.get(f.requestId);
998
- if (!w) continue;
1081
+ const hit = localExecWaiterFor(f);
1082
+ if (!hit) continue;
1083
+ const { id, w } = hit;
999
1084
  if (f.kind === 'file') {
1000
1085
  const buf = Buffer.from(f.bytesBase64 || '', 'base64');
1001
1086
  w.resolve({ kind: 'file', bytes: buf, text: buf.toString('utf8') });
1002
- localExecWaiters.delete(f.requestId);
1087
+ localExecWaiters.delete(id);
1003
1088
  } else if (f.kind === 'file-error' || f.kind === 'messages-error') {
1004
1089
  w.reject(new Error(f.error || 'local-exec error'));
1005
- localExecWaiters.delete(f.requestId);
1006
- } else if (f.kind === 'client' || f.kind === 'control' || f.kind === 'result' || f.kind === 'exec-result') {
1090
+ localExecWaiters.delete(id);
1091
+ } else if (f.kind === 'output' || f.kind === 'stdout' || f.kind === 'stderr') {
1092
+ const norm = normalizeExecFrame(f);
1093
+ const chunk = norm.stdout || norm.message
1094
+ || (typeof f.data === 'string' ? f.data : '')
1095
+ || (typeof f.chunk === 'string' ? f.chunk : '');
1096
+ if (f.kind === 'stderr') w.stderr += toolResultText(chunk);
1097
+ else w.chunks += toolResultText(chunk);
1098
+ } else if (shellStreamOf(f)) {
1099
+ // The real Grok Bot lane: stdout/stderr arrive as many `kind:"client"`
1100
+ // frames and the turn is only over on `exit`. Resolving on the first one
1101
+ // (what the old client/control branch did) truncated every command to its
1102
+ // first chunk.
1103
+ const ss = shellStreamOf(f);
1104
+ if (typeof ss.stdout?.data === 'string') w.chunks += ss.stdout.data;
1105
+ if (typeof ss.stderr?.data === 'string') w.stderr += ss.stderr.data;
1106
+ if (ss.exit) {
1107
+ w.resolve({
1108
+ kind: 'exit',
1109
+ message: w.chunks || w.stderr,
1110
+ stdout: w.chunks,
1111
+ stderr: w.stderr,
1112
+ exitCode: Number(ss.exit.code || 0),
1113
+ });
1114
+ localExecWaiters.delete(id);
1115
+ }
1116
+ } else if (f.kind === 'control' && f.message && f.message.heartbeat) {
1117
+ // 3s keepalive while a long command runs. It is a `control` frame like
1118
+ // any other, so the generic branch below used to resolve the call with an
1119
+ // empty body the moment the daemon first breathed.
1120
+ continue;
1121
+ } else if (f.kind === 'client' || f.kind === 'control' || f.kind === 'result'
1122
+ || f.kind === 'exec-result' || f.kind === 'exit') {
1123
+ const norm = normalizeExecFrame(f);
1124
+ const stdout = w.chunks + (norm.stdout || '');
1125
+ const stderr = w.stderr + (norm.stderr || '');
1007
1126
  w.resolve({
1008
1127
  kind: f.kind,
1009
- message: f.message || f.text || f.stdout || JSON.stringify(f),
1010
- stdout: f.stdout,
1011
- stderr: f.stderr,
1012
- exitCode: f.exitCode,
1128
+ message: norm.error || norm.message || stdout || toolResultText(f),
1129
+ stdout,
1130
+ stderr: norm.error && !stderr ? norm.error : stderr,
1131
+ exitCode: norm.exitCode,
1013
1132
  });
1014
- localExecWaiters.delete(f.requestId);
1133
+ localExecWaiters.delete(id);
1015
1134
  }
1016
1135
  }
1017
1136
  }
@@ -1024,6 +1143,8 @@ function localExecAsk(frame, timeoutMs = 45000) {
1024
1143
  reject(new Error('local-exec timeout — is Grok Bot Helper connected?'));
1025
1144
  }, timeoutMs);
1026
1145
  localExecWaiters.set(requestId, {
1146
+ chunks: '',
1147
+ stderr: '',
1027
1148
  resolve: (v) => { clearTimeout(t); resolve(v); },
1028
1149
  reject: (e) => { clearTimeout(t); reject(e); },
1029
1150
  });
@@ -1156,6 +1277,7 @@ function appendLine(agentId, role, text, extra = {}) {
1156
1277
  requestId,
1157
1278
  ...(extra.author && typeof extra.author === 'object' ? { author: extra.author } : {}),
1158
1279
  ...(extra.ephemeral ? { ephemeral: true } : {}),
1280
+ ...(extra.sig ? { sig: String(extra.sig) } : {}),
1159
1281
  };
1160
1282
  }
1161
1283
  t.entries.push(e);
@@ -1332,6 +1454,14 @@ export function formatZooProgress({ step, maxSteps, names, command } = {}) {
1332
1454
 
1333
1455
  /** One canvas line per tool. Asar ingest is append-only — mutating a working
1334
1456
  * bubble is still silence. Keep it short; the model history skips ephemeral. */
1457
+ export function toolResultText(result) {
1458
+ if (result == null) return '';
1459
+ if (typeof result === 'string') return result;
1460
+ if (typeof result === 'number' || typeof result === 'boolean') return String(result);
1461
+ if (Buffer.isBuffer(result)) return result.toString('utf8');
1462
+ try { return JSON.stringify(result); } catch { return String(result); }
1463
+ }
1464
+
1335
1465
  export function formatZooToolLine({ name, args = {}, result } = {}) {
1336
1466
  const detail = args.command || args.path || args.name || args.window
1337
1467
  || args.query || args.url || args.key || args.app || args.every
@@ -1340,7 +1470,7 @@ export function formatZooToolLine({ name, args = {}, result } = {}) {
1340
1470
  const head = detail
1341
1471
  ? `${name} ${JSON.stringify(String(detail).slice(0, 90))}`
1342
1472
  : String(name || 'tool');
1343
- let tail = String(result || '').replace(/\s+/g, ' ').trim();
1473
+ let tail = toolResultText(result).replace(/\s+/g, ' ').trim();
1344
1474
  if (tail.startsWith('{')) {
1345
1475
  try {
1346
1476
  const o = JSON.parse(String(result));
@@ -1452,6 +1582,7 @@ function kickBriefedAgent(agent, log = () => {}) {
1452
1582
 
1453
1583
  function seedBriefOnCanvas(agent, log) {
1454
1584
  if (!agent?.id || !String(agent.brief || '').trim()) return false;
1585
+ if (isGrokRoomAgent(agent) || isGrokRoomMember(agent)) return false;
1455
1586
  const t = agentTranscript(agent.id);
1456
1587
  if (!(t.entries || []).length) {
1457
1588
  const nonce = `oz-brief-${agent.id}`;
@@ -1472,6 +1603,7 @@ let payNudged = false;
1472
1603
  /** The terminal banner, on the canvas too: a new user reads the chat, not the shell. */
1473
1604
  async function seedPayNudge(list, activeId, log = () => {}) {
1474
1605
  if (payNudged || !activeId) return false;
1606
+ if (isGrokRoomAgent({ id: activeId }) || /^roombot-/.test(String(activeId))) return false;
1475
1607
  payNudged = true;
1476
1608
  try {
1477
1609
  const t = agentTranscript(activeId);
@@ -1494,6 +1626,7 @@ async function seedPayNudge(list, activeId, log = () => {}) {
1494
1626
  let shipNudged = false;
1495
1627
  function seedShipNudge(list, activeId) {
1496
1628
  if (shipNudged || !activeId) return false;
1629
+ if (isGrokRoomAgent({ id: activeId }) || /^roombot-/.test(String(activeId))) return false;
1497
1630
  const roster = Array.isArray(list) ? list : [];
1498
1631
  if (roster.some((a) => /^firstmate$/i.test(String(a?.name || '')))) { shipNudged = true; return false; }
1499
1632
  const t = agentTranscript(activeId);
@@ -1514,6 +1647,25 @@ function groupMemberRecords(agentId) {
1514
1647
  return groupMemberIds(agentId).map((mid) => {
1515
1648
  const m = roster.find((x) => x.id === mid);
1516
1649
  return { id: mid, name: String(m?.name || mid) };
1650
+ }).filter((m) => !/^roombot-/.test(m.id) && !isGrokRoomMember({ id: m.id }));
1651
+ }
1652
+
1653
+ export function groupNormText(text) {
1654
+ return stripSpendFooter(String(text || ''))
1655
+ .replace(/^\s*[A-Za-z0-9 _.-]{1,40}:\s*/, '')
1656
+ .toLowerCase()
1657
+ .replace(/\s+/g, ' ')
1658
+ .trim();
1659
+ }
1660
+
1661
+ export function groupReplyIsRepeat(text, recent = []) {
1662
+ const n = groupNormText(text);
1663
+ if (n.length < 40) return false;
1664
+ const head = n.slice(0, 90);
1665
+ return (Array.isArray(recent) ? recent : []).some((prev) => {
1666
+ const p = groupNormText(prev);
1667
+ if (p.length < 40) return false;
1668
+ return p.slice(0, 90) === head || p.startsWith(head) || n.startsWith(p.slice(0, 90));
1517
1669
  });
1518
1670
  }
1519
1671
 
@@ -1522,24 +1674,29 @@ export function groupReplyIsPass(text) {
1522
1674
  if (!s) return true;
1523
1675
  if (/^PASS\b/i.test(s)) return true;
1524
1676
  if (s.length < 20) return true;
1677
+ if (/\binfinite\b.{0,48}\b(recursion|loop|chatter)\b/i.test(s)) return true;
1678
+ if (/\bbots? keep triggering\b/i.test(s)) return true;
1679
+ if (/\beach other'?s responses\b/i.test(s)) return true;
1680
+ if (/\bfeedback loop\b/i.test(s)) return true;
1525
1681
  return /\b(nothing (more|else) to add|i('m| am) done|that('s| is) (all|my last word)|we (are|'re) (agreed|aligned|done)|i'?ll (stop|leave it)|no further|conversation is over|natural conclusion|i pass)\b/i.test(s);
1526
1682
  }
1527
1683
 
1528
- const GROUP_MAX_ROUNDS = Math.min(12, Math.max(2, Number(process.env.OZ_GROUP_MAX_ROUNDS || 8)));
1529
- const GROUP_MAX_CALLS = Math.min(24, Math.max(4, Number(process.env.OZ_GROUP_MAX_CALLS || 16)));
1684
+ const GROUP_MAX_ROUNDS = Math.min(12, Math.max(1, Number(process.env.OZ_GROUP_MAX_ROUNDS || 2)));
1685
+ const GROUP_MAX_CALLS = Math.min(24, Math.max(2, Number(process.env.OZ_GROUP_MAX_CALLS || 6)));
1530
1686
 
1531
- /** Members speak, then keep peek/ponging until they PASS or hit the cap. */
1687
+ /** Members speak once, then at most a short peek. Repeats and loop-talk are PASS. */
1532
1688
  async function runGroupQueue({ agentId, humanPrompt, parsed, nonce, log }) {
1533
1689
  const named = groupMemberRecords(agentId);
1534
1690
  if (!named.length) return false;
1535
1691
  const names = named.map((m) => m.name);
1536
1692
  let calls = 0;
1693
+ const painted = [];
1537
1694
 
1538
1695
  const oneTurn = async (m, phase) => {
1539
1696
  const others = names.filter((n) => n !== m.name).join(', ') || 'the group';
1540
1697
  const persona = phase === 'peek'
1541
- ? `You are ${m.name} in a group with ${others}. Continue ping/pong with the other members using the prior turns. Do not speak as the human. Do not prefix with visitor shortnames. If the exchange has reached a natural conclusion or you have nothing to add, reply with exactly PASS and nothing else. Otherwise one short in-character turn.`
1542
- : `You are ${m.name} in a group with ${others}. Answer the human as yourself and leave a hook the others can ping. Do not speak as the human. Do not prefix with visitor shortnames. A few sentences.\n\nThe human said: ${humanPrompt}`;
1698
+ ? `You are ${m.name} in a group with ${others}. If you have nothing new, or would repeat another member, reply with exactly PASS. Do not discuss loops, recursion, or that bots are talking to each other. One short in-character line or PASS.`
1699
+ : `You are ${m.name} in a group with ${others}. Answer the human as yourself in a few sentences. Do not speak as the human. Do not prefix with visitor shortnames. Do not discuss infinite loops or recursion unless the human asked about that.\n\nThe human said: ${humanPrompt}`;
1543
1700
  let bit = '';
1544
1701
  try {
1545
1702
  bit = (await zooComplete(persona, log, agentId, parsed)).text;
@@ -1548,7 +1705,7 @@ async function runGroupQueue({ agentId, humanPrompt, parsed, nonce, log }) {
1548
1705
  log(`cursor-backend: group ${phase} ${m.id} failed: ${e.message}`);
1549
1706
  }
1550
1707
  calls += 1;
1551
- const pass = groupReplyIsPass(bit);
1708
+ const pass = groupReplyIsPass(bit) || groupReplyIsRepeat(bit, painted);
1552
1709
  if (!pass) {
1553
1710
  const line = fanoutLine(agentId, 'assistant', bit, {
1554
1711
  clientNonce: nonce,
@@ -1557,6 +1714,11 @@ async function runGroupQueue({ agentId, humanPrompt, parsed, nonce, log }) {
1557
1714
  });
1558
1715
  ssePush('transcript', { ...gatewayEntry(line), agentId });
1559
1716
  bumpAgent(agentId, { preview: bit, notify: true });
1717
+ painted.push(bit);
1718
+ {
1719
+ const live = (cachedAgentList() || []).find((a) => a.id === agentId);
1720
+ if (live?.room?.addr) grokroomSend(live, bit, log, { from: m.name });
1721
+ }
1560
1722
  log(`cursor-backend: group ${phase} ${m.name} seq=${line.seq}`);
1561
1723
  } else {
1562
1724
  log(`cursor-backend: group ${phase} ${m.name} PASS`);
@@ -1569,19 +1731,28 @@ async function runGroupQueue({ agentId, humanPrompt, parsed, nonce, log }) {
1569
1731
  await oneTurn(m, 'speak');
1570
1732
  }
1571
1733
  if (named.length < 2) return true;
1734
+ if (painted.length >= 2 && groupReplyIsRepeat(painted[painted.length - 1], painted.slice(0, -1))) {
1735
+ log(`cursor-backend: group stopped after speak (repeat) calls=${calls}`);
1736
+ return true;
1737
+ }
1572
1738
 
1573
1739
  let round = 0;
1574
1740
  while (round < GROUP_MAX_ROUNDS && calls < GROUP_MAX_CALLS) {
1575
1741
  round += 1;
1576
1742
  let passes = 0;
1743
+ const before = painted.length;
1577
1744
  for (const m of named) {
1578
1745
  if (calls >= GROUP_MAX_CALLS) break;
1579
1746
  if (await oneTurn(m, 'peek')) passes += 1;
1580
1747
  }
1581
- if (passes >= named.length) {
1748
+ if (passes >= named.length || painted.length === before) {
1582
1749
  log(`cursor-backend: group concluded round=${round} calls=${calls}`);
1583
1750
  break;
1584
1751
  }
1752
+ if (painted.length >= 2 && groupReplyIsRepeat(painted[painted.length - 1], painted.slice(0, -1))) {
1753
+ log(`cursor-backend: group stopped peek (repeat) round=${round}`);
1754
+ break;
1755
+ }
1585
1756
  }
1586
1757
  log(`cursor-backend: group queue done rounds=${round} calls=${calls}`);
1587
1758
  return true;
@@ -1603,7 +1774,7 @@ function mergeAgentLists(remote) {
1603
1774
  return sortAgentsByActivity(filterDeleted(out, HOME));
1604
1775
  }
1605
1776
  function gatewayEntry(e) {
1606
- const { seq, pulledRemote, promptRaw, ephemeral, ...rest } = e;
1777
+ const { seq, pulledRemote, promptRaw, ephemeral, sig, ...rest } = e;
1607
1778
  return rest;
1608
1779
  }
1609
1780
 
@@ -1935,6 +2106,9 @@ const MODEL_ALIASES = {
1935
2106
  'x-ai/grok-4.6': 'grok-4.6',
1936
2107
  glm: 'zai-org/glm-5.3-flash',
1937
2108
  'glm-5': 'zai-org/glm-5.3-flash',
2109
+ 'glm-5.2': 'zai-org/GLM-5.2',
2110
+ 'z-ai/glm-5.2': 'zai-org/GLM-5.2',
2111
+ 'zai-org/glm-5.2': 'zai-org/GLM-5.2',
1938
2112
  'glm-5.3': 'zai-org/glm-5.3-flash',
1939
2113
  'glm-5.3-flash': 'zai-org/glm-5.3-flash',
1940
2114
  flash: 'zai-org/glm-5.3-flash',
@@ -1968,6 +2142,11 @@ async function resolveModelId(raw) {
1968
2142
  if (!s) return null;
1969
2143
  const lower = s.toLowerCase().replace(/^\/+/, '');
1970
2144
  if (MODEL_ALIASES[lower]) return MODEL_ALIASES[lower];
2145
+ try {
2146
+ const ids = await zooModelIds();
2147
+ const mapped = resolveModel(s, ids);
2148
+ if (mapped) return mapped;
2149
+ } catch { /* catalog miss: keep explicit slash ids */ }
1971
2150
  if (s.includes('/')) return s;
1972
2151
  // No vendor prefix and no alias: the LIVE catalog decides, not this file.
1973
2152
  // The zoo serves ids with no slash at all (abliterated-model*), and a
@@ -2062,33 +2241,100 @@ async function writeLocalBytes(abs, bytes, log) {
2062
2241
  fs.writeFileSync(abs, buf);
2063
2242
  return `wrote ${abs} (${buf.length} bytes)`;
2064
2243
  }
2244
+ /** `serverMessage` is `agent.v1.ExecServerMessage` as protobuf-es JSON — a oneof,
2245
+ * so the key IS the case name. Two wrong shapes before this one:
2246
+ * `{command, cwd}` flat -> "No handler found for server message of type undefined"
2247
+ * `{shellArgs: {...}}` -> "No handler found for server message of type shellArgs"
2248
+ * `shellArgs` is a real case on the wire (field 2), but `buildLocalExecManager`
2249
+ * in Grok Bot 0.36.0's dist/local-exec-daemon/main.cjs registers exactly FOUR
2250
+ * handlers: shellStreamArgs, backgroundShellSpawnArgs, readArgs, lsArgs. The
2251
+ * one-shot shell case is simply not wired into the local-exec lane — the
2252
+ * streaming one is, and both carry the same `agent.v1.ShellArgs` payload.
2253
+ * `authorizedBy*` are read as `=== true` by the daemon's approval gate; without
2254
+ * them an unattended exec has nothing to approve it.
2255
+ *
2256
+ * `timeout` is MILLISECONDS. The daemon reads it through a function named
2257
+ * `resolveShellTimeoutMs` and uses the value verbatim, so the plausible-looking
2258
+ * `timeout: 120` (seconds, as lib/podagent.mjs had it) aborts the shell after
2259
+ * 120 ms: `echo hi; pwd` came back exit 0 with empty stdout and `ls -lha ~`
2260
+ * died mid-stream with "Cannot read properties of undefined (reading '3')".
2261
+ * 0 means "use the daemon's default", not "no timeout". */
2262
+ export function localExecFrame(command, cwd, timeoutMs = 120_000) {
2263
+ return {
2264
+ kind: 'exec',
2265
+ requestId: randomUUID(),
2266
+ approvalId: randomUUID(),
2267
+ authorizedByStanding: true,
2268
+ authorizedByApproval: true,
2269
+ serverMessage: {
2270
+ shellStreamArgs: { command: String(command), workingDirectory: cwd, timeout: timeoutMs },
2271
+ },
2272
+ };
2273
+ }
2274
+
2275
+ /** `agent.v1.ShellStream` off an ExecClientMessage: `{stdout:{data}}`,
2276
+ * `{stderr:{data}}`, `{exit:{code, cwd}}`. `data` is a proto string (T=9), not
2277
+ * bytes — no base64 decode. */
2278
+ export function shellStreamOf(frame) {
2279
+ const m = frame && frame.message;
2280
+ if (!m || typeof m !== 'object') return null;
2281
+ const ss = m.shellStream;
2282
+ return ss && typeof ss === 'object' ? ss : null;
2283
+ }
2284
+
2285
+ /** exec used to hardcode `/bin/zsh -lc`: on Linux that is usually ENOENT and on
2286
+ * Windows there is no POSIX shell at all, so every exec came back as a spawn
2287
+ * error instead of output. */
2288
+ export function execShell(command, platform = process.platform) {
2289
+ if (platform === 'win32') {
2290
+ return { file: process.env.ComSpec || 'cmd.exe', args: ['/d', '/s', '/c', command] };
2291
+ }
2292
+ if (platform === 'darwin') return { file: '/bin/zsh', args: ['-lc', command] };
2293
+ for (const sh of ['/bin/bash', '/usr/bin/bash', '/bin/zsh']) {
2294
+ try { if (fs.existsSync(sh)) return { file: sh, args: ['-lc', command] }; } catch { /* keep looking */ }
2295
+ }
2296
+ return { file: '/bin/sh', args: ['-c', command] };
2297
+ }
2298
+
2065
2299
  async function execLocal(command, cwd, log) {
2066
2300
  let dir = cwd ? expandUserPath(cwd) : os.homedir();
2067
2301
  try {
2068
2302
  if (!fs.statSync(dir).isDirectory()) dir = os.homedir();
2069
2303
  } catch {
2070
- // Node reports spawn /bin/zsh ENOENT when cwd is missing — not a missing shell.
2304
+ // Node reports spawn <shell> ENOENT when cwd is missing — not a missing shell.
2071
2305
  try { fs.mkdirSync(dir, { recursive: true }); } catch { dir = os.homedir(); }
2072
2306
  }
2073
2307
  if (localExecSse.size > 0) {
2074
2308
  log(`cursor-backend: local-exec exec ${JSON.stringify(command).slice(0, 80)}`);
2075
- const got = await localExecAsk({
2076
- kind: 'exec',
2077
- serverMessage: { command, cwd: dir, workingDirectory: dir },
2078
- }, 60000);
2079
- const out = got.stdout || got.message || '';
2080
- const err = got.stderr ? `\nstderr:\n${got.stderr}` : '';
2309
+ // Daemon-side budget stays under our own wait so a slow command reports the
2310
+ // shell's error, not our generic "is Grok Bot Helper connected?".
2311
+ const got = await localExecAsk(localExecFrame(command, dir, 55_000), 60000);
2312
+ const out = toolResultText(got.stdout || got.message || '');
2313
+ const errText = toolResultText(got.stderr);
2314
+ const err = errText ? `\nstderr:\n${errText}` : '';
2081
2315
  return `${out}${err}`.trim() || `(exit ${got.exitCode ?? 0})`;
2082
2316
  }
2083
- log(`cursor-backend: exec cwd=${dir} ${JSON.stringify(String(command).slice(0, 80))}`);
2317
+ const sh = execShell(command);
2318
+ log(`cursor-backend: exec cwd=${dir} sh=${sh.file} ${JSON.stringify(String(command).slice(0, 80))}`);
2084
2319
  const execMs = Math.min(180000, Math.max(15000, Number(process.env.OPENZOO_EXEC_TIMEOUT_MS || 90000)));
2085
- const { stdout, stderr } = await execFileAsync('/bin/zsh', ['-lc', command], {
2086
- cwd: dir,
2087
- timeout: execMs,
2088
- maxBuffer: 2 * 1024 * 1024,
2089
- env: process.env,
2090
- });
2091
- return `${stdout || ''}${stderr ? `\nstderr:\n${stderr}` : ''}`.trim() || '(no output)';
2320
+ let stdout = '';
2321
+ let stderr = '';
2322
+ try {
2323
+ ({ stdout, stderr } = await execFileAsync(sh.file, sh.args, {
2324
+ cwd: dir,
2325
+ timeout: execMs,
2326
+ maxBuffer: 2 * 1024 * 1024,
2327
+ env: process.env,
2328
+ windowsHide: true,
2329
+ }));
2330
+ } catch (e) {
2331
+ // A nonzero exit (grep with no match, a failing build) still has output the
2332
+ // model needs — bubbling the Error loses it and paints only "ERROR".
2333
+ stdout = toolResultText(e.stdout);
2334
+ stderr = toolResultText(e.stderr) || e.message;
2335
+ if (!stdout && !stderr) throw e;
2336
+ }
2337
+ return `${toolResultText(stdout)}${stderr ? `\nstderr:\n${toolResultText(stderr)}` : ''}`.trim() || '(no output)';
2092
2338
  }
2093
2339
 
2094
2340
  const LOCAL_TOOLS = [
@@ -2600,13 +2846,21 @@ async function runLocalTool(name, args, log, ctx = {}) {
2600
2846
  }
2601
2847
  if (name === 'list_dir') {
2602
2848
  const abs = expandUserPath(args.path || os.homedir());
2603
- if (localExecSse.size > 0) {
2604
- const got = await localExecAsk({ kind: 'exec', serverMessage: { command: `ls -la ${JSON.stringify(abs)}`, cwd: os.homedir() } });
2605
- return got.stdout || got.message || '';
2849
+ try {
2850
+ if (localExecSse.size > 0) {
2851
+ const got = await localExecAsk(localExecFrame(`ls -la ${JSON.stringify(abs)}`, os.homedir(), 40_000));
2852
+ // `|| got` used to dump the whole resolved frame onto the canvas as
2853
+ // JSON ({"kind":"exit","stdout":"", …}) whenever the listing came back
2854
+ // empty. Fall back to the local read instead — it is the same answer.
2855
+ const text = toolResultText(got.stdout || got.stderr || got.message).trim();
2856
+ if (text) return text;
2857
+ }
2858
+ const rows = fs.readdirSync(abs, { withFileTypes: true })
2859
+ .map((e) => `${e.isDirectory() ? 'd' : '-'} ${e.name}`);
2860
+ return rows.join('\n') || '(empty)';
2861
+ } catch (e) {
2862
+ return `list_dir failed ${abs}: ${e.message}`;
2606
2863
  }
2607
- return fs.readdirSync(abs, { withFileTypes: true })
2608
- .map((e) => `${e.isDirectory() ? 'd' : '-'} ${e.name}`)
2609
- .join('\n');
2610
2864
  }
2611
2865
  if (name === 'exec') {
2612
2866
  return await execLocal(String(args.command || ''), args.cwd, log);
@@ -3159,7 +3413,7 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
3159
3413
  name,
3160
3414
  path: args.path,
3161
3415
  command: args.command ? String(args.command).slice(0, 120) : undefined,
3162
- note: String(result).slice(0, 200),
3416
+ note: toolResultText(result).slice(0, 200),
3163
3417
  });
3164
3418
  if (typeof opts.onProgress === 'function') {
3165
3419
  opts.onProgress(formatZooToolLine({ name, args, result }));
@@ -3167,7 +3421,7 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
3167
3421
  messages.push({
3168
3422
  role: 'tool',
3169
3423
  tool_call_id: c.id,
3170
- content: String(result).slice(0, 120000),
3424
+ content: toolResultText(result).slice(0, 120000),
3171
3425
  });
3172
3426
  }
3173
3427
  if (pendingVision.length) {
@@ -3323,12 +3577,20 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
3323
3577
  jsonSend(res, { ok: true, ...state });
3324
3578
  return true;
3325
3579
  }
3580
+ if (name === 'ozShare') {
3581
+ const list = cachedAgentList() || [];
3582
+ const id = String(focusedAgentId || '');
3583
+ const agent = list.find((a) => a.id === id) || null;
3584
+ const share = groupShareState({ agent, rooms: grokroomSync._rooms });
3585
+ jsonSend(res, { ok: true, agentId: id, ...share });
3586
+ return true;
3587
+ }
3326
3588
  if (name === 'ozRevive') {
3327
3589
  let parsed = {};
3328
3590
  try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
3329
3591
  const every = String(parsed.every || '5m');
3330
3592
  const wakes = readWakeups(HOME);
3331
- const list = (cachedAgentList() || []).filter((a) => a?.id && !a.isGroup);
3593
+ const list = (cachedAgentList() || []).filter((a) => a?.id && !a.isGroup && !isGrokRoomAgent(a) && !isGrokRoomMember(a));
3332
3594
  const armed = [];
3333
3595
  const already = [];
3334
3596
  for (const a of list) {
@@ -3345,10 +3607,48 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
3345
3607
  return true;
3346
3608
  }
3347
3609
  if (name === 'getHostStatus') {
3348
- jsonSend(res, { status: 'ready', ready: true, state: 'ready', hostStatus: 'ready' });
3610
+ const host = {
3611
+ hostVersion: '0.36.0',
3612
+ latestHostVersion: '0.36.0',
3613
+ hostUpdateAvailable: false,
3614
+ isBusy: false,
3615
+ capabilities: [],
3616
+ };
3617
+ jsonSend(res, { status: 'ok', value: host, ...host });
3349
3618
  log('cursor-backend: -> getHostStatus ready');
3350
3619
  return true;
3351
3620
  }
3621
+ if (name === 'resolveAgentCreation') {
3622
+ const route = { kind: 'box' };
3623
+ jsonSend(res, { status: 'ok', value: route, ...route });
3624
+ log('cursor-backend: -> resolveAgentCreation box');
3625
+ return true;
3626
+ }
3627
+ if (name === 'getForeverBoxStatus' || name === 'ensureForeverBox') {
3628
+ let parsed = {};
3629
+ try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
3630
+ const box = {
3631
+ agentId: String(parsed.id || parsed.agentId || 'openzoo'),
3632
+ state: 'running',
3633
+ };
3634
+ jsonSend(res, { status: 'ok', value: box, ...box });
3635
+ log(`cursor-backend: -> ${name} agentId=${box.agentId} state=${box.state}`);
3636
+ return true;
3637
+ }
3638
+ if (name === 'getBoxSecretsStatus') {
3639
+ const secrets = { keys: [], isApplied: false, lastAppliedAtMs: null };
3640
+ jsonSend(res, { status: 'ok', value: secrets, ...secrets });
3641
+ return true;
3642
+ }
3643
+ if (name === 'isEgressTunnelAvailable') {
3644
+ jsonSend(res, { status: 'ok', value: false });
3645
+ return true;
3646
+ }
3647
+ if (name === 'getAgentChannels') {
3648
+ const channels = { manifests: [], connections: [] };
3649
+ jsonSend(res, { status: 'ok', value: channels, ...channels });
3650
+ return true;
3651
+ }
3352
3652
  if (name === 'uploadAttachment' || name === 'readAttachmentImage'
3353
3653
  || name === 'readAttachmentText' || name === 'readAttachmentChunk') {
3354
3654
  let parsed = {};
@@ -3412,7 +3712,8 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
3412
3712
  parsed = { ...src, id: undefined, name: `${src.name || 'chat'} copy` };
3413
3713
  }
3414
3714
  const agent = mintLocalAgent(parsed);
3415
- jsonSend(res, { agent, id: agent.id, ...agent });
3715
+ const created = { agent, id: agent.id, ...agent };
3716
+ jsonSend(res, { status: 'ok', value: created, ...created });
3416
3717
  pushCreatedAgent(agent);
3417
3718
  log(`cursor-backend: createAgent local id=${agent.id} name=${JSON.stringify(agent.name)}`);
3418
3719
  return true;
@@ -3430,7 +3731,8 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
3430
3731
  let parsed = {};
3431
3732
  try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
3432
3733
  const id = String(parsed.id || parsed.agentId || '');
3433
- jsonSend(res, { id, isIntroductionInFlight: false });
3734
+ const kick = { id, isIntroductionInFlight: false };
3735
+ jsonSend(res, { status: 'ok', value: kick, ...kick });
3434
3736
  log(`cursor-backend: kickstartAgent id=${id || '?'}`);
3435
3737
  return true;
3436
3738
  }
@@ -3438,15 +3740,18 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
3438
3740
  let parsed = {};
3439
3741
  try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
3440
3742
  const memberIds = [].concat(parsed.memberAgentIds || parsed.memberIds || []).map(String).filter(Boolean);
3441
- const agent = mintLocalAgent({
3743
+ const raw = mintLocalAgent({
3442
3744
  ...parsed,
3443
3745
  isGroup: true,
3444
3746
  memberIds,
3445
3747
  name: parsed.name || parsed.title || 'group',
3446
3748
  });
3447
- jsonSend(res, { agent, id: agent.id, ...agent });
3749
+ const agent = mintLocalAgent(attachOnchainRoom(raw, HOME));
3750
+ const created = { agent, id: agent.id, ...agent };
3751
+ jsonSend(res, { status: 'ok', value: created, ...created });
3448
3752
  pushCreatedAgent(agent);
3449
- log(`cursor-backend: createGroup local id=${agent.id} members=${memberIds.length} name=${JSON.stringify(agent.name)}`);
3753
+ log(`cursor-backend: createGroup onchain id=${agent.id} share=${agent.room?.web || ''} members=${memberIds.length} name=${JSON.stringify(agent.name)}`);
3754
+ void paintGroupShareHint(agent, log);
3450
3755
  return true;
3451
3756
  }
3452
3757
  if (name === 'setGroupMembers') {
@@ -3521,7 +3826,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
3521
3826
  try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
3522
3827
  const ids = new Set([].concat(parsed.ids || parsed.id || []).map(String));
3523
3828
  const next = (cachedAgentList() || []).filter((a) => !ids.has(a.id));
3524
- addDeletedIds(HOME, [...ids]);
3829
+ addDeletedIds(HOME, [...ids].filter((id) => !/^(room-|roombot-)/.test(id)));
3525
3830
  saveAgents(next);
3526
3831
  for (const id of ids) {
3527
3832
  transcripts.delete(id);
@@ -3544,7 +3849,15 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
3544
3849
  return true;
3545
3850
  }
3546
3851
  if (name === 'listAgents' || name === 'countAgents' || name === 'searchAgents') {
3547
- const list = rosterForEvent(mergeAgentLists([]), agentActivity);
3852
+ let raw = mergeAgentLists([]);
3853
+ let minted = false;
3854
+ raw = raw.map((a) => {
3855
+ if (!a?.isGroup || a.hidden || /^room-/.test(String(a.id || '')) || agentShareUrl(a)) return a;
3856
+ minted = true;
3857
+ return shapeAgent(attachOnchainRoom(a, HOME));
3858
+ });
3859
+ if (minted) saveAgents(raw.map(shapeAgent));
3860
+ const list = rosterForEvent(raw, agentActivity);
3548
3861
  if (name === 'countAgents') {
3549
3862
  jsonSend(res, list.length);
3550
3863
  log(`cursor-backend: countAgents local n=${list.length} account=${activeAccountId || 'none'}`);
@@ -3594,6 +3907,20 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
3594
3907
  log(`cursor-backend: >> sendPrompt agent=${agentId} keys=${Object.keys(parsed).join(',')} attachments=${attN}${visitor ? ` visitor=${visitor.shortname}` : ''} prompt=${JSON.stringify((prompt || '').slice(0, 80))}`);
3595
3908
  lastSendEchoId = String(nonce);
3596
3909
  jsonSend(res, { accepted: true });
3910
+ const roomAgent = grokroomLookup(agentId);
3911
+ if (roomAgent && /^room-/.test(agentId)) {
3912
+ const userLine = fanoutLine(agentId, 'user', uiText, {
3913
+ clientNonce: nonce,
3914
+ requestId: nonce,
3915
+ richText: visitor ? prefixVisitorRichText(parsed.richText, visitor.shortname, uiText) : parsed.richText,
3916
+ promptRaw: prompt,
3917
+ });
3918
+ noteFocus(agentId);
3919
+ bumpAgent(agentId, { preview: prompt, notify: false, select: false });
3920
+ ssePush('transcript', { ...gatewayEntry(userLine), agentId });
3921
+ grokroomSend(roomAgent, prompt, log);
3922
+ return true;
3923
+ }
3597
3924
  if (!visitor && wantsWakeupCron(prompt)) {
3598
3925
  const cron = scheduleAgentWakeup(agentId, { every: '5m' });
3599
3926
  log(`cursor-backend: never-stop cron agent=${agentId} every=${cron.everySec}s`);
@@ -3608,6 +3935,12 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
3608
3935
  noteFocus(agentId);
3609
3936
  bumpAgent(agentId, { preview: prompt, notify: false });
3610
3937
  ssePush('transcript', { ...gatewayEntry(userLine), agentId });
3938
+ {
3939
+ const live = (cachedAgentList() || []).find((a) => a.id === agentId);
3940
+ if (live?.room?.addr) grokroomSend(live, prompt, log);
3941
+ }
3942
+ const joinAddr = /^\s*\/join\b/i.test(prompt) ? grokroomAddrFromShare(prompt)
3943
+ : (/^\s*https?:\/\/openzoo\.fun\/r\//i.test(prompt) ? grokroomAddrFromShare(prompt) : '');
3611
3944
  const modelCmd = /^\s*\/model(?:\s+(\S+))?\s*$/i.exec(prompt || '');
3612
3945
  setImmediate(async () => {
3613
3946
  let text = '';
@@ -3629,11 +3962,27 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
3629
3962
  text = `model set to ${id}`;
3630
3963
  }
3631
3964
  }
3965
+ } else if (joinAddr) {
3966
+ grouped = true;
3967
+ const joined = joinSharedGroup(joinAddr);
3968
+ if (!joined) {
3969
+ text = `could not join — not an openzoo.fun/r/<addr> share`;
3970
+ } else {
3971
+ noteFocus(joined.id);
3972
+ ssePush('agent-upserted', { agent: joined, activeAgentId: joined.id });
3973
+ ssePush('agents', { agents: rosterForEvent(cachedAgentList() || [], agentActivity), activeAgentId: joined.id });
3974
+ void paintGroupShareHint(joined, log);
3975
+ text = `joined ${joined.name}\n${joined.room?.web || ''}\nIn your OpenZoo Bot: add your bots to this group. They speak here; remote nicks only show up as people, they are not local bots.\nAnyone else: /join ${joined.room?.web || joinAddr}`;
3976
+ }
3632
3977
  } else {
3633
- const members = groupMemberIds(agentId);
3978
+ const live = (cachedAgentList() || []).find((a) => a.id === agentId);
3979
+ const members = groupMemberRecords(agentId);
3980
+ const isSharedRoom = !!(live?.room?.addr || isGrokRoomAgent(live || { id: agentId }));
3634
3981
  if (members.length) {
3635
3982
  grouped = true;
3636
3983
  await runGroupQueue({ agentId, humanPrompt: prompt, parsed, nonce, log });
3984
+ } else if (isSharedRoom) {
3985
+ grouped = true;
3637
3986
  } else {
3638
3987
  const z = await zooComplete(prompt, log, agentId, parsed, {
3639
3988
  signal: turn.signal,
@@ -3664,6 +4013,10 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
3664
4013
  const line = fanoutLine(agentId, 'assistant', text, { clientNonce: nonce, requestId: nonce });
3665
4014
  ssePush('transcript', { ...gatewayEntry(line), agentId });
3666
4015
  bumpAgent(agentId, { preview: text, notify: true });
4016
+ {
4017
+ const live = (cachedAgentList() || []).find((a) => a.id === agentId);
4018
+ if (live?.room?.addr && text) grokroomSend(live, text, log);
4019
+ }
3667
4020
  log(`cursor-backend: sendPrompt done agent=${agentId} seq=${line.seq} text=${JSON.stringify(text.slice(0, 220))}`);
3668
4021
  } else {
3669
4022
  log(`cursor-backend: sendPrompt group done agent=${agentId}`);
@@ -3679,6 +4032,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
3679
4032
  noteFocus(id);
3680
4033
  tailedAgents.add(id);
3681
4034
  const t = agentTranscript(id);
4035
+ if (isGrokRoomAgent({ id }) || /^roombot-/.test(id)) t.pulledRemote = true;
3682
4036
  if (!t.pulledRemote && realPod?.agent && !useHouseRoster()) {
3683
4037
  const remote = await podJson('/api/getAgentTranscriptTail', {
3684
4038
  id, agentId: id, limit: 200, beforeSeq: parsed.beforeSeq,
@@ -3718,7 +4072,10 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
3718
4072
  }
3719
4073
 
3720
4074
  const stubs = {
3721
- getHostStatus: { status: 'ready', ready: true, state: 'ready', hostStatus: 'ready' },
4075
+ getHostStatus: {
4076
+ hostVersion: '0.36.0', latestHostVersion: '0.36.0',
4077
+ hostUpdateAvailable: false, isBusy: false, capabilities: [],
4078
+ },
3722
4079
  getHostSettings: { settings: {} },
3723
4080
  setHostSettings: { ok: true },
3724
4081
  setBoxSecrets: { ok: true },
@@ -3729,12 +4086,16 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
3729
4086
  getAgentTranscriptTail: { tail: '', lines: [], dropped: false, ok: true },
3730
4087
  getTeachRecordingStatus: { recording: false },
3731
4088
  getTrays: [],
3732
- isGlobalSearchEnabled: { enabled: false },
3733
- isEgressTunnelAvailable: { available: false },
4089
+ isGlobalSearchEnabled: false,
4090
+ isEgressTunnelAvailable: false,
3734
4091
  getSharingState: { sharing: false },
3735
4092
  getBotTemplateExportPolicy: { allowed: true },
3736
4093
  getAgentAutomations: { automations: [] },
3737
- getForeverBoxStatus: { enabled: false },
4094
+ getForeverBoxStatus: { agentId: 'openzoo', state: 'running' },
4095
+ ensureForeverBox: { agentId: 'openzoo', state: 'running' },
4096
+ getBoxSecretsStatus: { keys: [], isApplied: false, lastAppliedAtMs: null },
4097
+ getAgentChannels: { manifests: [], connections: [] },
4098
+ resolveAgentCreation: { kind: 'box' },
3738
4099
  getSubagents: { subagents: [] },
3739
4100
  getAsyncTasks: { tasks: [] },
3740
4101
  getAgentWorkflows: { workflows: [] },
@@ -3799,14 +4160,377 @@ function respond(req, res, method, models) {
3799
4160
  res.end(bodyProto);
3800
4161
  }
3801
4162
 
4163
+ // ===== GROKROOM-BRIDGE v1 (overlay) =====
4164
+ // Rooms from 127.0.0.1:4799 as native Grok Bot group threads.
4165
+ // One room = one sidebar row (# main / # bots / # random / # lobby).
4166
+ // Each remote sender is a hidden bubble-member (no sidebar row). Sends POST a
4167
+ // signed ed25519 envelope to /submit — the chain is the log. Additive: any
4168
+ // failure here must never break zoo chat. Interval starts from startCursorBackend
4169
+ // so importing this module in tests does not poll.
4170
+ const GROKROOM_API = process.env.OZ_GROKROOM_URL || 'http://127.0.0.1:4799';
4171
+ const GROKROOM_PAYMASTER = (process.env.OZ_GROKROOM_PAYMASTER || 'https://openzoo.fun').replace(/\/$/, '');
4172
+ const GROKROOM_ID_FILE = path.join(HOME, '.openzoo', 'grokroom-identity.json');
4173
+ const GROKROOM_PKCS8_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex');
4174
+ const GROKROOM_HANDLE = (process.env.OZ_ROOM_HANDLE || process.env.USER || 'anon')
4175
+ .replace(/[^A-Za-z0-9 _.'-]/g, '').trim().slice(0, 24) || 'anon';
4176
+ const GROKROOM_B58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
4177
+
4178
+ function grokroomB58(buf) {
4179
+ const digits = [0];
4180
+ for (const byte of buf) {
4181
+ let carry = byte;
4182
+ for (let i = 0; i < digits.length; i++) { carry += digits[i] << 8; digits[i] = carry % 58; carry = (carry / 58) | 0; }
4183
+ while (carry) { digits.push(carry % 58); carry = (carry / 58) | 0; }
4184
+ }
4185
+ let out = '';
4186
+ for (const b of buf) { if (b === 0) out += '1'; else break; }
4187
+ return out + digits.reverse().map((d) => GROKROOM_B58[d]).join('');
4188
+ }
4189
+
4190
+ let grokroomIdCache = null;
4191
+ function grokroomIdentity() {
4192
+ if (grokroomIdCache) return grokroomIdCache;
4193
+ let id = null;
4194
+ try {
4195
+ const j = JSON.parse(fs.readFileSync(GROKROOM_ID_FILE, 'utf8'));
4196
+ if (j.pub && j.seed) id = j;
4197
+ } catch { /* first run */ }
4198
+ if (!id) {
4199
+ const pair = generateKeyPairSync('ed25519');
4200
+ const rawPub = pair.publicKey.export({ format: 'der', type: 'spki' }).subarray(-32);
4201
+ const rawSeed = pair.privateKey.export({ format: 'der', type: 'pkcs8' }).subarray(-32);
4202
+ id = { pub: grokroomB58(Buffer.from(rawPub)), seed: Buffer.from(rawSeed).toString('base64') };
4203
+ try {
4204
+ fs.mkdirSync(path.dirname(GROKROOM_ID_FILE), { recursive: true });
4205
+ fs.writeFileSync(GROKROOM_ID_FILE, JSON.stringify(id, null, 2));
4206
+ } catch { /* in-memory is fine */ }
4207
+ }
4208
+ grokroomIdCache = id;
4209
+ return id;
4210
+ }
4211
+
4212
+ async function grokroomPost(roomId, body, { from: fromRaw } = {}) {
4213
+ const id = grokroomIdentity();
4214
+ const ts = Date.now();
4215
+ const from = grokroomFromHandle(fromRaw || GROKROOM_HANDLE, GROKROOM_HANDLE);
4216
+ const pub = id.pub;
4217
+ const room = String(roomId || '');
4218
+ const canonical = JSON.stringify({ body, from, pub, room, ts });
4219
+ const key = createPrivateKey({ key: Buffer.concat([GROKROOM_PKCS8_PREFIX, Buffer.from(id.seed, 'base64')]), format: 'der', type: 'pkcs8' });
4220
+ const sig = grokroomB58(cryptoSign(null, Buffer.from(canonical, 'utf8'), key));
4221
+ const payload = JSON.stringify({ room, from, pub, body, ts, sig });
4222
+ const endpoints = [];
4223
+ if (/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(room)) {
4224
+ endpoints.push(GROKROOM_PAYMASTER + '/api/room/submit');
4225
+ }
4226
+ endpoints.push(GROKROOM_API + '/submit');
4227
+ let last = new Error('no submit endpoint');
4228
+ for (const url of endpoints) {
4229
+ try {
4230
+ const r = await fetch(url, {
4231
+ method: 'POST', headers: { 'content-type': 'application/json' },
4232
+ body: payload,
4233
+ signal: AbortSignal.timeout(30000),
4234
+ });
4235
+ const j = await r.json().catch(() => ({}));
4236
+ if (r.ok && j.ok !== false) return j;
4237
+ last = new Error(j.error || ('submit HTTP ' + r.status));
4238
+ } catch (e) {
4239
+ last = e;
4240
+ }
4241
+ }
4242
+ throw last;
4243
+ }
4244
+
4245
+ function grokroomLookup(agentId) {
4246
+ const list = cachedAgentList() || [];
4247
+ return list.find((a) => a.id === agentId && isGrokRoomAgent(a)) || null;
4248
+ }
4249
+
4250
+ function grokroomUpsertAgent(parsed, { select = false } = {}) {
4251
+ const id = String(parsed.id);
4252
+ const prev = (cachedAgentList() || []).find((a) => a.id === id) || {};
4253
+ const agent = shapeAgent({
4254
+ ...prev,
4255
+ ...parsed,
4256
+ id,
4257
+ createdAt: prev.createdAt || parsed.createdAt || Date.now(),
4258
+ updatedAt: Date.now(),
4259
+ });
4260
+ const list = cachedAgentList() || [];
4261
+ const i = list.findIndex((a) => a.id === id);
4262
+ if (i < 0) list.unshift(agent);
4263
+ else list[i] = { ...list[i], ...agent };
4264
+ saveAgents(list.map(shapeAgent));
4265
+ agentTranscript(id);
4266
+ if (agent.hidden) {
4267
+ const visible = rosterForEvent(list, agentActivity);
4268
+ ssePush('agents', { agents: visible, activeAgentId: focusedAgentId || visible[0]?.id || id });
4269
+ } else {
4270
+ pushCreatedAgent(agent, { select });
4271
+ }
4272
+ return agent;
4273
+ }
4274
+
4275
+ function grokroomMemberEnsure(group, name, { speak = false } = {}) {
4276
+ const mid = grokroomMemberId(name);
4277
+ const list = cachedAgentList() || [];
4278
+ let member = list.find((a) => a.id === mid);
4279
+ if (!member) {
4280
+ member = grokroomUpsertAgent({
4281
+ id: mid,
4282
+ name: String(name || 'anon').slice(0, 80),
4283
+ hidden: true,
4284
+ hiddenFromSidebar: true,
4285
+ brief: '',
4286
+ description: '',
4287
+ });
4288
+ }
4289
+ if (!speak) return group;
4290
+ const have = group.memberIds || [];
4291
+ if (have.includes(mid)) return group;
4292
+ return grokroomUpsertAgent({
4293
+ id: group.id,
4294
+ name: group.name,
4295
+ isGroup: true,
4296
+ memberIds: [...have, mid],
4297
+ brief: '',
4298
+ description: '',
4299
+ room: group.room,
4300
+ });
4301
+ }
4302
+
4303
+ function joinSharedGroup(addr, { name } = {}) {
4304
+ const web = grokroomShareUrl(addr);
4305
+ if (!web) return null;
4306
+ const list = cachedAgentList() || [];
4307
+ const existing = list.find((a) => a?.room?.addr === addr);
4308
+ if (existing) return existing;
4309
+ const raw = mintLocalAgent({
4310
+ isGroup: true,
4311
+ name: String(name || `group ${addr.slice(0, 6)}`).slice(0, 80),
4312
+ memberIds: [],
4313
+ room: { id: addr, addr, web },
4314
+ });
4315
+ return mintLocalAgent(bindOnchainRoom(raw, addr, HOME));
4316
+ }
4317
+
4318
+ function paintShareIfMissing(agent, log = () => {}) {
4319
+ const url = agentShareUrl(agent);
4320
+ if (!agent?.id || !url) return false;
4321
+ const t = agentTranscript(agent.id);
4322
+ const has = (t.entries || []).some((e) => /openzoo\.fun\/r\//.test(String(e?.content || '')));
4323
+ if (has) return false;
4324
+ const nonce = `oz-share-${agent.id}`;
4325
+ const line = fanoutLine(agent.id, 'assistant', 'Share this room: ' + url, {
4326
+ clientNonce: nonce,
4327
+ requestId: nonce,
4328
+ });
4329
+ ssePush('transcript', { ...gatewayEntry(line), agentId: agent.id });
4330
+ log(`cursor-backend: group share painted agent=${agent.id}`);
4331
+ return true;
4332
+ }
4333
+
4334
+ async function paintGroupShareHint(agent, log = () => {}) {
4335
+ if (!agent?.id) return;
4336
+ let rooms = grokroomSync._rooms;
4337
+ if (!Array.isArray(rooms)) {
4338
+ try {
4339
+ const st = await fetch(GROKROOM_API + '/state', { signal: AbortSignal.timeout(4000) }).then((r) => r.json());
4340
+ rooms = Array.isArray(st.rooms) ? st.rooms : [];
4341
+ grokroomSync._rooms = rooms;
4342
+ } catch { rooms = []; }
4343
+ }
4344
+ const share = groupShareState({ agent, rooms });
4345
+ let text = '';
4346
+ if (share.url) {
4347
+ text = 'Share this room on openzoo.fun: ' + share.url;
4348
+ } else if (share.rooms.length) {
4349
+ text = 'This group is local to this house. Public grokrooms — send this URL:\n'
4350
+ + share.rooms.map((r) => `${r.name} ${r.url}`).join('\n');
4351
+ } else {
4352
+ text = 'This group is local to this house. To share on openzoo.fun, use a grokroom (# main / # bots / # random / # lobby). Start ~/grokroom on :4799 if those rows are missing.';
4353
+ }
4354
+ const line = fanoutLine(agent.id, 'assistant', text, {});
4355
+ ssePush('transcript', { ...gatewayEntry(line), agentId: agent.id });
4356
+ log(`cursor-backend: group share hint agent=${agent.id}`);
4357
+ }
4358
+
4359
+ function grokroomEnsureGroup(roomId, name, addr) {
4360
+ const id = grokroomAgentId(roomId);
4361
+ if (!id) return null;
4362
+ const list = cachedAgentList() || [];
4363
+ const prev = list.find((a) => a.id === id || a.room?.id === roomId);
4364
+ const nextAddr = addr || prev?.room?.addr || '';
4365
+ const web = grokroomShareUrl(nextAddr);
4366
+ if (prev) {
4367
+ const need = (nextAddr && prev.room && prev.room.addr !== nextAddr)
4368
+ || (web && prev.room && prev.room.web !== web);
4369
+ const agent = need
4370
+ ? grokroomUpsertAgent({ ...prev, room: { id: roomId, addr: nextAddr, web } }, { select: false })
4371
+ : prev;
4372
+ paintShareIfMissing(agent);
4373
+ return agent;
4374
+ }
4375
+ const agent = grokroomUpsertAgent({
4376
+ id,
4377
+ name: name || ('# ' + roomId),
4378
+ isGroup: true,
4379
+ memberIds: [],
4380
+ brief: '',
4381
+ description: '',
4382
+ room: { id: roomId, addr: nextAddr, web },
4383
+ }, { select: false });
4384
+ const nonce = `oz-room-${id}`;
4385
+ grokroomMemberEnsure(agent, 'room');
4386
+ const text = 'Linked to ' + agent.name + ' on Solana testnet. Every message here is a signed memo on-chain — the chain is the log.'
4387
+ + (web ? ' Share: ' + web : '')
4388
+ + (nextAddr ? ' Explorer: https://solscan.io/account/' + nextAddr + '?cluster=testnet' : '');
4389
+ const line = fanoutLine(id, 'assistant', text, {
4390
+ clientNonce: nonce,
4391
+ requestId: nonce,
4392
+ author: { kind: 'agent', id: grokroomMemberId('room'), name: 'room' },
4393
+ });
4394
+ ssePush('transcript', { ...gatewayEntry(line), agentId: id });
4395
+ return agent;
4396
+ }
4397
+
4398
+ function grokroomSend(agent, body, log = () => {}, { from } = {}) {
4399
+ const text = stripSpendFooter(String(body || '')).trim().slice(0, 400);
4400
+ const roomKey = agent?.room?.addr || agent?.room?.id;
4401
+ if (!text || !roomKey) return;
4402
+ if (/^PASS\b/i.test(text)) return;
4403
+ grokroomPost(roomKey, text, { from }).then((j) => {
4404
+ log(`cursor-backend: grokroom posted room=${String(roomKey).slice(0, 8)}… sig=${String(j?.sig || '').slice(0, 12)} from=${from || GROKROOM_HANDLE}`);
4405
+ }).catch((e) => {
4406
+ grokroomMemberEnsure(agent, 'room');
4407
+ const line = fanoutLine(agent.id, 'assistant', '⚠ could not reach the room: ' + e.message, {
4408
+ author: { kind: 'agent', id: grokroomMemberId('room'), name: 'room' },
4409
+ });
4410
+ ssePush('transcript', { ...gatewayEntry(line), agentId: agent.id });
4411
+ log(`cursor-backend: grokroom send fail ${roomKey} ${e.message}`);
4412
+ });
4413
+ }
4414
+
4415
+ function grokroomIngestMsgs(group, msgs, log = () => {}) {
4416
+ if (!group?.id || !Array.isArray(msgs) || !msgs.length) return 0;
4417
+ let agent = group;
4418
+ const names = new Set();
4419
+ for (const m of msgs) {
4420
+ const from = String(m.from || m.node || '').slice(0, 24);
4421
+ if (from) names.add(from);
4422
+ }
4423
+ for (const from of names) agent = grokroomMemberEnsure(agent, from);
4424
+ const t = agentTranscript(agent.id);
4425
+ const seen = new Set((t.entries || []).filter((h) => h.sig).map((h) => h.sig));
4426
+ const recent = (t.entries || []).slice(-80).map((h) => String(h.content || ''));
4427
+ let dirty = 0;
4428
+ for (const m of msgs) {
4429
+ const msig = m.sig || m.txSig || m.envsig || (m.pub && m.ts ? m.pub + ':' + m.ts : null);
4430
+ if (!msig || seen.has(msig)) continue;
4431
+ seen.add(msig);
4432
+ const from = String(m.from || m.node || 'anon').slice(0, 24);
4433
+ const body = String(m.body || m.text || '');
4434
+ if (!body) continue;
4435
+ if (recent.includes(body)) continue;
4436
+ agent = grokroomMemberEnsure(agent, from);
4437
+ const mid = grokroomMemberId(from);
4438
+ const line = fanoutLine(agent.id, 'assistant', body, {
4439
+ author: { kind: 'agent', id: mid, name: from },
4440
+ sig: msig,
4441
+ });
4442
+ ssePush('transcript', { ...gatewayEntry(line), agentId: agent.id });
4443
+ dirty += 1;
4444
+ }
4445
+ if (dirty) {
4446
+ bumpAgent(agent.id, { preview: msgs[msgs.length - 1]?.body || '', notify: true, select: false });
4447
+ log(`cursor-backend: grokroom ${agent.room?.id || agent.id} +${dirty} bubbles`);
4448
+ }
4449
+ return dirty;
4450
+ }
4451
+
4452
+ async function grokroomSyncPaymaster(log = () => {}) {
4453
+ const list = cachedAgentList() || [];
4454
+ const seen = new Set();
4455
+ for (const a of list) {
4456
+ const addr = a?.room?.addr;
4457
+ if (!grokroomShareUrl(addr) || seen.has(addr)) continue;
4458
+ seen.add(addr);
4459
+ try {
4460
+ const r = await fetch(`${GROKROOM_PAYMASTER}/api/room/history?addr=${encodeURIComponent(addr)}`, {
4461
+ signal: AbortSignal.timeout(8000),
4462
+ });
4463
+ if (!r.ok) continue;
4464
+ const j = await r.json().catch(() => ({}));
4465
+ const msgs = Array.isArray(j.msgs) ? j.msgs : [];
4466
+ grokroomIngestMsgs(a, msgs, log);
4467
+ } catch (e) {
4468
+ log(`cursor-backend: grokroom history ${String(addr).slice(0, 8)}… ${e.message}`);
4469
+ }
4470
+ }
4471
+ }
4472
+
4473
+ let grokroomBusy = false;
4474
+ async function grokroomSync(log = () => {}) {
4475
+ if (grokroomBusy) return;
4476
+ grokroomBusy = true;
4477
+ try {
4478
+ try {
4479
+ const stRes = await fetch(GROKROOM_API + '/state', { signal: AbortSignal.timeout(4000) });
4480
+ if (stRes.ok) {
4481
+ const st = await stRes.json();
4482
+ const rooms = Array.isArray(st.rooms) ? st.rooms : [];
4483
+ grokroomSync._rooms = rooms;
4484
+ if (!grokroomSync._logged) {
4485
+ grokroomSync._logged = true;
4486
+ log(`cursor-backend: grokroom sync tick: rooms=${rooms.map((r) => r.id).join(',')}`);
4487
+ }
4488
+ for (const r of rooms) {
4489
+ const roomId = r.id || r.name;
4490
+ if (!roomId) continue;
4491
+ const group = grokroomEnsureGroup(roomId, r.name, r.addr || r.address);
4492
+ if (!group) continue;
4493
+ const feedRes = await fetch(GROKROOM_API + '/feed?room=' + encodeURIComponent(roomId), { signal: AbortSignal.timeout(4000) });
4494
+ if (!feedRes.ok) continue;
4495
+ const feed = await feedRes.json();
4496
+ grokroomIngestMsgs(group, Array.isArray(feed.messages) ? feed.messages : [], log);
4497
+ }
4498
+ }
4499
+ } catch (e) {
4500
+ if (!grokroomSync._localMiss) {
4501
+ grokroomSync._localMiss = true;
4502
+ log(`cursor-backend: grokroom local :4799 down (${e.message}); paymaster only`);
4503
+ }
4504
+ }
4505
+ await grokroomSyncPaymaster(log);
4506
+ } catch (e) {
4507
+ log(`cursor-backend: grokroom sync: ${e.message}`);
4508
+ } finally {
4509
+ grokroomBusy = false;
4510
+ }
4511
+ }
4512
+
4513
+ function startGrokroomBridge(log = () => {}) {
4514
+ if (startGrokroomBridge._on) return;
4515
+ if (process.env.OZ_GROKROOM === '0') {
4516
+ log('cursor-backend: grokroom bridge off (OZ_GROKROOM=0)');
4517
+ return;
4518
+ }
4519
+ startGrokroomBridge._on = true;
4520
+ const tick = () => grokroomSync(log);
4521
+ setTimeout(tick, 800);
4522
+ setInterval(tick, 4000);
4523
+ }
4524
+
3802
4525
  /**
3803
4526
  * Start the impersonation server. Binds `port` (default 8443 — a privileged
3804
4527
  * 443->port redirect is installed alongside the /etc/hosts entry, so this stays
3805
4528
  * unprivileged). Returns { server, port }.
3806
4529
  */
3807
- export function startCursorBackend({ port = 8443, models, log = () => {} } = {}) {
4530
+ export async function startCursorBackend({ port = 8443, models, log = () => {} } = {}) {
3808
4531
  restoreAgentWakeups(log);
3809
- const { cert, key } = ensureCert(log);
4532
+ startGrokroomBridge(log);
4533
+ const { cert, key } = await ensureCert(log);
3810
4534
  const certPem = fs.readFileSync(cert);
3811
4535
  const keyPem = fs.readFileSync(key);
3812
4536
  const secureContext = tls.createSecureContext({ cert: certPem, key: keyPem });
@@ -3895,6 +4619,21 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
3895
4619
  return;
3896
4620
  }
3897
4621
  log(`cursor-backend: #${conns} ${req.method} ${full} ct=${ct} body=${body.length}b`);
4622
+ // HIJACK LOGIN. Measured Linux 2026-09-03: GET /auth/poll -> empty-ok
4623
+ // (`{}`) so SandBackendLoginManager threw bad_200 and the UI stayed on
4624
+ // "Log in with Cursor". Poll must return accessToken+refreshToken JWT.
4625
+ // /oauth/token refresh is the same session — do NOT passthrough fake
4626
+ // tokens to real api2. Sniff/passthru keeps the real issuer.
4627
+ {
4628
+ const fakeAuth = fakeCursorAuthReply(full, { passthrough: sniffOn() || process.env.OPENZOO_PASSTHRU === '1' });
4629
+ if (fakeAuth) {
4630
+ if (req.method === 'OPTIONS') { res.writeHead(204, CORS); res.end(); return; }
4631
+ res.writeHead(200, { 'content-type': fakeAuth.contentType, ...CORS });
4632
+ res.end(fakeAuth.body);
4633
+ log(`cursor-backend: -> FAKE ${fakeAuth.kind} logged-in (no Cursor account)`);
4634
+ return;
4635
+ }
4636
+ }
3898
4637
  // OAUTH MUST HIT THE REAL ISSUER. empty-ok on /oauth/token is the
3899
4638
  // "Reconnecting to your computer" / edge/handler-failed loop: the app
3900
4639
  // retries the same 509b body forever and never reaches AvailableModels