clay-server 2.31.0 → 2.32.0-beta.1

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 (74) hide show
  1. package/lib/browser-mcp-server.js +32 -44
  2. package/lib/debate-mcp-server.js +14 -31
  3. package/lib/mcp-local.js +31 -1
  4. package/lib/project-connection.js +4 -2
  5. package/lib/project-filesystem.js +47 -1
  6. package/lib/project-http.js +75 -8
  7. package/lib/project-mcp.js +4 -0
  8. package/lib/project-sessions.js +88 -51
  9. package/lib/project-user-message.js +12 -7
  10. package/lib/project.js +204 -90
  11. package/lib/public/app.js +123 -448
  12. package/lib/public/codex-avatar.png +0 -0
  13. package/lib/public/css/debate.css +3 -2
  14. package/lib/public/css/filebrowser.css +91 -1
  15. package/lib/public/css/icon-strip.css +21 -5
  16. package/lib/public/css/input.css +181 -100
  17. package/lib/public/css/mates.css +43 -0
  18. package/lib/public/css/mention.css +48 -4
  19. package/lib/public/css/menus.css +1 -1
  20. package/lib/public/css/messages.css +2 -0
  21. package/lib/public/css/notifications-center.css +19 -0
  22. package/lib/public/index.html +46 -24
  23. package/lib/public/modules/app-connection.js +138 -37
  24. package/lib/public/modules/app-cursors.js +18 -17
  25. package/lib/public/modules/app-debate-ui.js +9 -9
  26. package/lib/public/modules/app-dm.js +170 -131
  27. package/lib/public/modules/app-favicon.js +28 -26
  28. package/lib/public/modules/app-header.js +79 -68
  29. package/lib/public/modules/app-home-hub.js +55 -47
  30. package/lib/public/modules/app-loop-ui.js +34 -18
  31. package/lib/public/modules/app-loop-wizard.js +6 -6
  32. package/lib/public/modules/app-messages.js +195 -152
  33. package/lib/public/modules/app-misc.js +23 -12
  34. package/lib/public/modules/app-notifications.js +91 -3
  35. package/lib/public/modules/app-panels.js +203 -49
  36. package/lib/public/modules/app-projects.js +159 -150
  37. package/lib/public/modules/app-rate-limit.js +5 -4
  38. package/lib/public/modules/app-rendering.js +149 -101
  39. package/lib/public/modules/app-skills-install.js +4 -4
  40. package/lib/public/modules/context-sources.js +12 -41
  41. package/lib/public/modules/dom-refs.js +21 -0
  42. package/lib/public/modules/filebrowser.js +173 -2
  43. package/lib/public/modules/input.js +86 -0
  44. package/lib/public/modules/mate-sidebar.js +38 -0
  45. package/lib/public/modules/mention.js +24 -6
  46. package/lib/public/modules/scheduler.js +1 -1
  47. package/lib/public/modules/sidebar-mates.js +66 -34
  48. package/lib/public/modules/sidebar-mobile.js +34 -30
  49. package/lib/public/modules/sidebar-projects.js +60 -57
  50. package/lib/public/modules/sidebar-sessions.js +75 -69
  51. package/lib/public/modules/sidebar.js +12 -20
  52. package/lib/public/modules/skills.js +8 -9
  53. package/lib/public/modules/sticky-notes.js +1 -2
  54. package/lib/public/modules/store.js +9 -2
  55. package/lib/public/modules/stt.js +4 -1
  56. package/lib/public/modules/tools.js +14 -9
  57. package/lib/sdk-bridge.js +511 -1113
  58. package/lib/sdk-message-processor.js +123 -134
  59. package/lib/sdk-worker.js +4 -0
  60. package/lib/server-dm.js +1 -0
  61. package/lib/server.js +86 -1
  62. package/lib/sessions.js +47 -36
  63. package/lib/ws-schema.js +2 -0
  64. package/lib/yoke/adapters/claude-worker.js +559 -0
  65. package/lib/yoke/adapters/claude.js +1418 -0
  66. package/lib/yoke/adapters/codex.js +968 -0
  67. package/lib/yoke/adapters/gemini.js +668 -0
  68. package/lib/yoke/codex-app-server.js +307 -0
  69. package/lib/yoke/index.js +199 -0
  70. package/lib/yoke/instructions.js +62 -0
  71. package/lib/yoke/interface.js +92 -0
  72. package/lib/yoke/mcp-bridge-server.js +294 -0
  73. package/lib/yoke/package.json +7 -0
  74. package/package.json +3 -1
package/lib/sdk-bridge.js CHANGED
@@ -1,10 +1,7 @@
1
1
  const crypto = require("crypto");
2
2
  var fs = require("fs");
3
3
  var path = require("path");
4
- var os = require("os");
5
- var net = require("net");
6
- var { execSync, spawn } = require("child_process");
7
- var { resolveOsUserInfo } = require("./os-users");
4
+ var { execSync } = require("child_process");
8
5
  var usersModule = require("./users");
9
6
  var { splitShellSegments, attachSkillDiscovery } = require("./sdk-skill-discovery");
10
7
  var { createMessageQueue } = require("./sdk-message-queue");
@@ -74,17 +71,26 @@ function mergeMcpServers(localServers, getRemoteFn) {
74
71
  merged[lk[i]] = localServers[lk[i]];
75
72
  hasAny = true;
76
73
  }
74
+ console.log("[mergeMcpServers] local servers:", lk.join(", ") || "(none)");
75
+ } else {
76
+ console.log("[mergeMcpServers] local servers: null");
77
77
  }
78
78
  if (typeof getRemoteFn === "function") {
79
79
  var remote = getRemoteFn();
80
80
  if (remote) {
81
81
  var rk = Object.keys(remote);
82
+ console.log("[mergeMcpServers] remote servers:", rk.join(", ") || "(none)");
82
83
  for (var j = 0; j < rk.length; j++) {
83
84
  merged[rk[j]] = remote[rk[j]];
84
85
  hasAny = true;
85
86
  }
87
+ } else {
88
+ console.log("[mergeMcpServers] remote servers: null/empty");
86
89
  }
90
+ } else {
91
+ console.log("[mergeMcpServers] getRemoteFn not a function");
87
92
  }
93
+ console.log("[mergeMcpServers] merged result:", Object.keys(merged).join(", ") || "(none)");
88
94
  return hasAny ? merged : null;
89
95
  }
90
96
 
@@ -95,12 +101,16 @@ function createSDKBridge(opts) {
95
101
  var send = opts.send; // broadcast to all clients
96
102
  var pushModule = opts.pushModule;
97
103
  var getNotificationsModule = opts.getNotificationsModule || function () { return null; };
98
- var getSDK = opts.getSDK;
104
+ var adapter = opts.adapter;
105
+ var adapters = opts.adapters || {};
99
106
  var mateDisplayName = opts.mateDisplayName || "";
100
107
  var isMate = opts.isMate || (slug.indexOf("mate-") === 0);
101
108
  var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
102
109
  var mcpServers = opts.mcpServers || null;
103
110
  var getRemoteMcpServers = opts.getRemoteMcpServers || null;
111
+ var clayPort = opts.clayPort || 2633;
112
+ var clayTls = opts.clayTls || false;
113
+ var clayAuthToken = opts.clayAuthToken || null;
104
114
  var onProcessingChanged = opts.onProcessingChanged || function () {};
105
115
  var onTurnDone = opts.onTurnDone || null;
106
116
 
@@ -121,11 +131,10 @@ function createSDKBridge(opts) {
121
131
  _idleReaperTimer = setInterval(function () {
122
132
  var now = Date.now();
123
133
  sm.sessions.forEach(function (session) {
124
- // Skip sessions that are actively processing, have no query, use workers,
134
+ // Skip sessions that are actively processing, have no query,
125
135
  // or are single-turn (Ralph Loop — managed by onQueryComplete).
126
136
  if (session.isProcessing) return;
127
137
  if (!session.queryInstance) return;
128
- if (session.worker) return;
129
138
  if (session.singleTurn) return;
130
139
  if (session.destroying) return;
131
140
 
@@ -134,9 +143,12 @@ function createSDKBridge(opts) {
134
143
  console.log("[sdk-bridge] Reaping idle session " + session.localId +
135
144
  " (idle " + Math.round((now - lastActivity) / 60000) + "min)" +
136
145
  (session.title ? " title=" + JSON.stringify(session.title) : ""));
137
- // End the message queue so the for-await loop in processQueryStream
146
+ // End the query so the for-await loop in processQueryStream
138
147
  // exits naturally, triggering the finally block cleanup.
139
- if (session.messageQueue && typeof session.messageQueue.end === "function") {
148
+ // Works for both in-process (messageQueue.end) and worker (handle.close) paths.
149
+ if (session.queryInstance && typeof session.queryInstance.close === "function") {
150
+ try { session.queryInstance.close(); } catch (e) {}
151
+ } else if (session.messageQueue && typeof session.messageQueue.end === "function") {
140
152
  try { session.messageQueue.end(); } catch (e) {}
141
153
  }
142
154
  }
@@ -167,7 +179,7 @@ function createSDKBridge(opts) {
167
179
  mateDisplayName: mateDisplayName,
168
180
  pushModule: pushModule,
169
181
  getNotificationsModule: getNotificationsModule,
170
- getSDK: getSDK,
182
+ adapter: adapter,
171
183
  onProcessingChanged: onProcessingChanged,
172
184
  onTurnDone: onTurnDone,
173
185
  opts: opts,
@@ -223,817 +235,48 @@ function createSDKBridge(opts) {
223
235
  });
224
236
  }
225
237
 
226
- // --- Worker process management (OS-level multi-user) ---
227
-
228
- var WORKER_SCRIPT = path.join(__dirname, "sdk-worker.js");
229
238
 
230
- // Ensure the package directory tree is world-readable so OS-level users
231
- // can access the worker script and its dependencies (the install path
232
- // may be under /root/.npm/_npx/ which defaults to 700)
233
- (function ensurePackageReadable() {
239
+ // --- Linux user project directory setup ---
240
+ // Ensures the linux user's .claude project directory exists and is writable,
241
+ // then pre-copies CLI session file if needed. Called before starting a query
242
+ // so the worker can resume from the correct session file.
243
+ function ensureLinuxUserProjectDir(linuxUser, session) {
234
244
  try {
235
- // Walk up from __dirname to find the package root (where node_modules lives)
236
- var pkgDir = path.join(__dirname, "..");
237
- // Open read+execute on each ancestor directory up to and including the
238
- // npx cache entry so that non-root users can traverse the path
239
- var dir = pkgDir;
240
- var dirs = [];
241
- while (dir !== path.dirname(dir)) {
242
- dirs.push(dir);
243
- dir = path.dirname(dir);
244
- }
245
- // Open o+rx on each ancestor so non-root users can traverse the path
246
- // (e.g. /root/.npm/_npx/.../node_modules/clay-server needs /root to be o+x)
247
- for (var di = 0; di < dirs.length; di++) {
248
- try {
249
- var st = fs.statSync(dirs[di]);
250
- // Add o+x (traverse) to all ancestors, o+rx to npm cache dirs
251
- var isNpmDir = dirs[di].indexOf(".npm") !== -1 || dirs[di].indexOf("node_modules") !== -1;
252
- var needed = isNpmDir ? 0o005 : 0o001; // rx for npm dirs, just x for ancestors like /root
253
- if ((st.mode & needed) !== needed) {
254
- fs.chmodSync(dirs[di], st.mode | needed);
255
- }
256
- } catch (e) {}
257
- }
258
- // Recursively make the package AND hoisted dependencies readable.
259
- // npm/npx may hoist deps (e.g. @anthropic-ai/claude-agent-sdk) to the
260
- // parent node_modules/ instead of inside clay-server/node_modules/.
261
- var { execSync: chmodExec } = require("child_process");
262
- // Find the top-level node_modules that contains clay-server
263
- var topNodeModules = path.join(pkgDir, "..");
264
- if (path.basename(topNodeModules) === "node_modules") {
265
- chmodExec("chmod -R o+rX " + JSON.stringify(topNodeModules), { stdio: "ignore", timeout: 15000 });
266
- } else {
267
- chmodExec("chmod -R o+rX " + JSON.stringify(pkgDir), { stdio: "ignore", timeout: 5000 });
268
- }
269
- } catch (e) {}
270
- })();
271
-
272
- // resolveLinuxUser delegates to shared os-users utility
273
- function resolveLinuxUser(username) {
274
- return resolveOsUserInfo(username);
275
- }
276
-
277
- /**
278
- * Spawn an SDK worker process running as the given Linux user.
279
- * Returns a worker handle with send/kill/event methods.
280
- */
281
- function spawnWorker(linuxUser) {
282
- var userInfo = resolveLinuxUser(linuxUser);
283
- var socketId = crypto.randomUUID();
284
- var socketPath = path.join(os.tmpdir(), "clay-worker-" + socketId + ".sock");
285
-
286
- var worker = {
287
- process: null,
288
- connection: null,
289
- socketPath: socketPath,
290
- server: null,
291
- messageHandlers: [],
292
- ready: false,
293
- readyPromise: null,
294
- _readyResolve: null,
295
- buffer: "",
296
- };
297
-
298
- worker.readyPromise = new Promise(function(resolve) {
299
- worker._readyResolve = resolve;
300
- });
301
-
302
- // Resolves when the worker process actually exits.
303
- // Used to prevent spawning a new worker before the old one finishes
304
- // flushing SDK session state to disk (race condition on resume).
305
- worker.exitPromise = new Promise(function(resolve) {
306
- worker._exitResolve = resolve;
307
- });
308
-
309
- // Create Unix socket server
310
- var spawnT0 = Date.now();
311
- worker.server = net.createServer(function(connection) {
312
- console.log("[PERF] spawnWorker: socket connection accepted +" + (Date.now() - spawnT0) + "ms");
313
- worker.connection = connection;
314
- connection.on("data", function(chunk) {
315
- worker.buffer += chunk.toString();
316
- var lines = worker.buffer.split("\n");
317
- worker.buffer = lines.pop();
318
- for (var i = 0; i < lines.length; i++) {
319
- if (!lines[i].trim()) continue;
245
+ var configMod = require("./config");
246
+ var osUsersMod = require("./os-users");
247
+ var originalHome = configMod.REAL_HOME || require("os").homedir();
248
+ var linuxUserHome = osUsersMod.getLinuxUserHome(linuxUser);
249
+ var uid = osUsersMod.getLinuxUserUid(linuxUser);
250
+ if (originalHome !== linuxUserHome && uid != null) {
251
+ var projectSlug = (cwd || "").replace(/\//g, "-");
252
+ var dstDir = path.join(linuxUserHome, ".claude", "projects", projectSlug);
253
+ // Create and chown the project directory once
254
+ if (!fs.existsSync(dstDir)) {
255
+ fs.mkdirSync(dstDir, { recursive: true });
256
+ try { require("child_process").execSync("chown -R " + uid + " " + JSON.stringify(path.join(linuxUserHome, ".claude"))); } catch (e2) {}
257
+ } else {
320
258
  try {
321
- var msg = JSON.parse(lines[i]);
322
- if (msg.type === "ready") {
323
- console.log("[PERF] spawnWorker: 'ready' IPC received +" + (Date.now() - spawnT0) + "ms");
324
- worker.ready = true;
325
- if (worker._readyResolve) {
326
- worker._readyResolve();
327
- worker._readyResolve = null;
328
- }
329
- }
330
- for (var h = 0; h < worker.messageHandlers.length; h++) {
331
- worker.messageHandlers[h](msg);
332
- }
333
- } catch (e) {
334
- console.error("[sdk-bridge] Failed to parse worker message:", e.message);
335
- }
336
- }
337
- });
338
- connection.on("error", function(err) {
339
- console.error("[sdk-bridge] Worker connection error:", err.message);
340
- });
341
- });
342
-
343
- worker.server.listen(socketPath, function() {
344
- console.log("[PERF] spawnWorker: socket listen ready +" + (Date.now() - spawnT0) + "ms");
345
- // Set socket permissions so the target user can connect
346
- try { fs.chmodSync(socketPath, 0o777); } catch (e) {}
347
-
348
- // Spawn worker process as the target Linux user.
349
- // Build a minimal, isolated env (no daemon env leakage).
350
- var workerEnv = require("./build-user-env").buildUserEnv({
351
- uid: userInfo.uid,
352
- gid: userInfo.gid,
353
- home: userInfo.home,
354
- user: linuxUser,
355
- shell: userInfo.shell || "/bin/bash",
356
- });
357
-
358
- console.log("[sdk-bridge] Spawning worker: uid=" + userInfo.uid + " gid=" + userInfo.gid + " cwd=" + cwd + " socket=" + socketPath);
359
- console.log("[sdk-bridge] Worker script: " + WORKER_SCRIPT);
360
- console.log("[sdk-bridge] Node: " + process.execPath);
361
- worker.process = spawn(process.execPath, [WORKER_SCRIPT, socketPath], {
362
- uid: userInfo.uid,
363
- gid: userInfo.gid,
364
- env: workerEnv,
365
- cwd: cwd,
366
- stdio: ["ignore", "pipe", "pipe"],
367
- });
368
-
369
- worker.process.stdout.on("data", function(data) {
370
- console.log("[sdk-worker:" + linuxUser + "] " + data.toString().trim());
371
- });
372
- worker._stderrBuf = "";
373
- worker.process.stderr.on("data", function(data) {
374
- var text = data.toString().trim();
375
- worker._stderrBuf += text + "\n";
376
- console.error("[sdk-worker:" + linuxUser + "] " + text);
377
- });
378
-
379
- worker.process.on("exit", function(code, signal) {
380
- console.log("[sdk-bridge] Worker for " + linuxUser + " exited (code=" + code + ", signal=" + signal + ")" + (worker._stderrBuf ? " stderr: " + worker._stderrBuf.trim() : ""));
381
- // Reject readyPromise if worker dies before becoming ready
382
- if (!worker.ready && worker._readyResolve) {
383
- worker._readyResolve = null;
384
- // Let the readyPromise hang; the query_error handler will clean up
385
- }
386
- // Notify message handlers about unexpected exit so sessions don't hang.
387
- // Always dispatch a fallback query_error. The handler is idempotent:
388
- // it checks isProcessing before taking action, and cleanupSessionWorker
389
- // guards against stale workers. This covers all exit cases including
390
- // signal kills (code=null) and normal exits where the IPC query_error
391
- // was lost due to connection timing.
392
- console.log("[sdk-bridge] Exit handler: pid=" + (worker.process ? worker.process.pid : "?") + " ready=" + worker.ready + " _queryEnded=" + worker._queryEnded + " _abortSent=" + worker._abortSent + " handlers=" + worker.messageHandlers.length);
393
- if (code === 0 && !worker.ready) {
394
- // Worker exited cleanly before sending "ready"
395
- for (var h = 0; h < worker.messageHandlers.length; h++) {
396
- worker.messageHandlers[h]({
397
- type: "query_error",
398
- error: "Worker exited before ready (code=0). stderr: " + (worker._stderrBuf || "(none)"),
399
- exitCode: 0,
400
- stderr: worker._stderrBuf || null,
401
- });
402
- }
403
- } else if (code !== 0 || code === null || signal) {
404
- // Worker crashed, was killed by signal, or exited abnormally
405
- var stderrText = worker._stderrBuf || "";
406
- var exitReason = signal
407
- ? "Worker killed by " + signal
408
- : (stderrText || "Worker exited with code " + code);
409
- for (var h = 0; h < worker.messageHandlers.length; h++) {
410
- worker.messageHandlers[h]({
411
- type: "query_error",
412
- error: exitReason,
413
- exitCode: code,
414
- stderr: stderrText || null,
415
- });
416
- }
417
- } else if (worker.messageHandlers.length > 0) {
418
- // Normal exit (code=0, ready=true). Dispatch fallback in case the
419
- // IPC query_done/query_error was lost (e.g. connection closed early).
420
- var fallbackMsg = worker._abortSent
421
- ? "Worker aborted"
422
- : "Worker exited before query completed";
423
- for (var h = 0; h < worker.messageHandlers.length; h++) {
424
- worker.messageHandlers[h]({
425
- type: "query_error",
426
- error: fallbackMsg,
427
- exitCode: 0,
428
- stderr: worker._stderrBuf || null,
429
- _fallback: true,
430
- });
431
- }
432
- }
433
- cleanupWorker(worker);
434
- if (worker._exitResolve) {
435
- worker._exitResolve();
436
- worker._exitResolve = null;
437
- }
438
- });
439
- });
440
-
441
- worker.send = function(msg) {
442
- if (!worker.connection || worker.connection.destroyed) return;
443
- try {
444
- worker.connection.write(JSON.stringify(msg) + "\n");
445
- } catch (e) {
446
- console.error("[sdk-bridge] Failed to send to worker:", e.message);
447
- }
448
- };
449
-
450
- worker.onMessage = function(handler) {
451
- worker.messageHandlers.push(handler);
452
- };
453
-
454
- worker.kill = function() {
455
- console.log("[sdk-bridge] worker.kill() called, pid=" + (worker.process ? worker.process.pid : "?") + " stack=" + new Error().stack.split("\n").slice(1, 4).join(" | "));
456
- worker.send({ type: "shutdown" });
457
- // Force kill after 5 seconds if still alive (gives SDK time to save session)
458
- setTimeout(function() {
459
- if (worker.process && !worker.process.killed) {
460
- try { worker.process.kill("SIGKILL"); } catch (e) {}
461
- }
462
- }, 5000);
463
- // Don't call cleanupWorker here. Let the exit handler do it after
464
- // the worker has had time to save SDK session state to disk.
465
- // Closing the connection prematurely causes the worker to exit
466
- // before the SDK can flush its session file, leading to "no
467
- // conversation found" errors on resume (OS multi-user mode).
468
- };
469
-
470
- return worker;
471
- }
472
-
473
- function cleanupWorker(worker) {
474
- console.log("[sdk-bridge] cleanupWorker() called, pid=" + (worker.process ? worker.process.pid : "?") + " stack=" + new Error().stack.split("\n").slice(1, 4).join(" | "));
475
- if (worker._abortTimeout) { clearTimeout(worker._abortTimeout); worker._abortTimeout = null; }
476
- if (worker.connection && !worker.connection.destroyed) {
477
- try { worker.connection.end(); } catch (e) {}
478
- }
479
- if (worker.server) {
480
- try { worker.server.close(); } catch (e) {}
481
- }
482
- // Remove socket file
483
- try { fs.unlinkSync(worker.socketPath); } catch (e) {}
484
- worker.ready = false;
485
- }
486
-
487
- /**
488
- * Start a query via a worker process running as the target Linux user.
489
- * Mirrors the in-process startQuery flow but delegates SDK execution to the worker.
490
- */
491
- async function startQueryViaWorker(session, text, images, linuxUser) {
492
- var t0 = session._queryStartTs || Date.now();
493
- function perf(label) { console.log("[PERF] sdk-bridge: " + label + " +" + (Date.now() - t0) + "ms"); }
494
- perf("startQueryViaWorker entered");
495
-
496
- // Wait for the previous worker to fully exit before spawning a new one.
497
- // Without this, the new worker may try to resume the SDK session file
498
- // while the old worker is still flushing it to disk (800ms grace period),
499
- // causing "no conversation found" and losing all prior context.
500
- if (session._workerExitPromise) {
501
- perf("waiting for old worker exit");
502
- var exitWait = session._workerExitPromise;
503
- session._workerExitPromise = null;
504
- await Promise.race([
505
- exitWait,
506
- new Promise(function(resolve) { setTimeout(resolve, 3000); }),
507
- ]);
508
- perf("old worker exit wait done");
509
- }
510
-
511
- // Reuse existing worker if alive, otherwise spawn a new one.
512
- // Spawn FIRST so the worker starts booting while we do dir setup below.
513
- var worker;
514
- var reusingWorker = false;
515
- if (session.worker && session.worker.ready && session.worker.process && !session.worker.process.killed) {
516
- worker = session.worker;
517
- reusingWorker = true;
518
- // Clear old message handlers so they don't fire for the new query
519
- worker.messageHandlers = [];
520
- worker._queryEnded = false;
521
- worker._abortSent = false;
522
- perf("reusing existing worker pid=" + (worker.process ? worker.process.pid : "?"));
523
- } else {
524
- try {
525
- perf("spawning new worker");
526
- worker = spawnWorker(linuxUser);
527
- perf("spawnWorker returned");
528
- session.worker = worker;
529
- } catch (e) {
530
- session.isProcessing = false;
531
- onProcessingChanged();
532
- sendAndRecord(session, { type: "error", text: "Failed to spawn worker for " + linuxUser + ": " + (e.message || e) });
533
- sendAndRecord(session, { type: "done", code: 1 });
534
- sm.broadcastSessionList();
535
- return;
536
- }
537
- }
538
-
539
- // Ensure the linux user's .claude project directory exists and is writable,
540
- // then pre-copy CLI session file if needed. This runs while the worker is
541
- // booting (readyPromise pending), so it adds no extra latency.
542
- perf("dir setup start");
543
- if (linuxUser) {
544
- try {
545
- var configMod = require("./config");
546
- var osUsersMod = require("./os-users");
547
- var originalHome = configMod.REAL_HOME || require("os").homedir();
548
- var linuxUserHome = osUsersMod.getLinuxUserHome(linuxUser);
549
- var uid = osUsersMod.getLinuxUserUid(linuxUser);
550
- if (originalHome !== linuxUserHome && uid != null) {
551
- var projectSlug = (cwd || "").replace(/\//g, "-");
552
- var dstDir = path.join(linuxUserHome, ".claude", "projects", projectSlug);
553
- // Create and chown the project directory once
554
- if (!fs.existsSync(dstDir)) {
555
- fs.mkdirSync(dstDir, { recursive: true });
556
- try { require("child_process").execSync("chown -R " + uid + " " + JSON.stringify(path.join(linuxUserHome, ".claude"))); } catch (e2) {}
557
- } else {
558
- try {
559
- var dirStat = fs.statSync(dstDir);
560
- if (dirStat.uid !== uid) {
561
- require("child_process").execSync("chown " + uid + " " + JSON.stringify(dstDir));
562
- }
563
- } catch (e2) {}
564
- }
565
- // Pre-copy CLI session file so the worker can resume the conversation
566
- if (session.cliSessionId) {
567
- var sessionFileName = session.cliSessionId + ".jsonl";
568
- var srcFile = path.join(originalHome, ".claude", "projects", projectSlug, sessionFileName);
569
- var dstFile = path.join(dstDir, sessionFileName);
570
- if (fs.existsSync(srcFile) && !fs.existsSync(dstFile)) {
571
- fs.copyFileSync(srcFile, dstFile);
572
- try { require("child_process").execSync("chown " + uid + " " + JSON.stringify(dstFile)); } catch (e2) {}
573
- console.log("[sdk-bridge] Pre-copied CLI session " + session.cliSessionId + " to " + linuxUser);
574
- }
575
- }
576
- }
577
- } catch (copyErr) {
578
- console.log("[sdk-bridge] Dir setup / session pre-copy skipped:", copyErr.message);
579
- }
580
- }
581
- perf("dir setup done");
582
-
583
- session.messageQueue = "worker"; // sentinel: messages go via worker IPC
584
- session.blocks = {};
585
- session.sentToolResults = {};
586
- session.activeTaskToolIds = {};
587
- session.pendingElicitations = {};
588
- session.streamedText = false;
589
- session.responsePreview = "";
590
- session.abortController = { abort: function() {
591
- console.log("[sdk-bridge] ABORT sent to worker pid=" + (worker.process ? worker.process.pid : "?"));
592
- worker._abortSent = true;
593
- try { worker.send({ type: "abort" }); } catch (e) {}
594
- // If the worker doesn't finish within 5s (e.g. subagent stuck), force-kill it.
595
- // The worker exit handler will dispatch a fallback query_error and send done.
596
- if (worker._abortTimeout) clearTimeout(worker._abortTimeout);
597
- worker._abortTimeout = setTimeout(function() {
598
- if (worker.process && !worker.process.killed && session.isProcessing) {
599
- console.log("[sdk-bridge] Abort timeout: force-killing worker pid=" + (worker.process ? worker.process.pid : "?"));
600
- try { worker.process.kill("SIGKILL"); } catch (e) {}
601
- }
602
- }, 5000);
603
- } };
604
-
605
- // Build initial user message content
606
- var content = [];
607
- if (images && images.length > 0) {
608
- for (var i = 0; i < images.length; i++) {
609
- content.push({
610
- type: "image",
611
- source: { type: "base64", media_type: images[i].mediaType, data: images[i].data },
612
- });
613
- }
614
- }
615
- if (text) {
616
- content.push({ type: "text", text: text });
617
- }
618
-
619
- var initialMessage = {
620
- type: "user",
621
- message: { role: "user", content: content },
622
- };
623
-
624
- // Build serializable query options (no callbacks, no AbortController)
625
- var queryOptions = {
626
- cwd: cwd,
627
- settingSources: ["user", "project", "local"],
628
- includePartialMessages: true,
629
- enableFileCheckpointing: true,
630
- extraArgs: { "replay-user-messages": null },
631
- promptSuggestions: true,
632
- agentProgressSummaries: true,
633
- };
634
-
635
- // MCP servers contain circular references (McpServer instances) and cannot
636
- // be serialized for IPC. Instead, extract serializable tool descriptors and
637
- // proxy tool calls back to the daemon via IPC.
638
- var _mergedMcp = mergeMcpServers(mcpServers, getRemoteMcpServers);
639
- var _mcpDescriptors = extractMcpDescriptors(_mergedMcp);
640
- // Do NOT put _mergedMcp into queryOptions; the worker will reconstruct
641
- // MCP servers from descriptors with IPC-proxied handlers.
642
-
643
- // Per-loop settings override global defaults when present
644
- var ls2 = session.loopSettings || {};
645
-
646
- if (ls2.model || sm.currentModel) queryOptions.model = ls2.model || sm.currentModel;
647
- if (ls2.effort || sm.currentEffort) queryOptions.effort = ls2.effort || sm.currentEffort;
648
- if (sm.currentBetas && sm.currentBetas.length > 0) queryOptions.betas = sm.currentBetas;
649
-
650
- var thinkingMode2 = ls2.thinking || sm.currentThinking;
651
- if (thinkingMode2 === "disabled") {
652
- queryOptions.thinking = { type: "disabled" };
653
- } else if (thinkingMode2 === "budget") {
654
- var budgetTokens2 = ls2.thinkingBudget || sm.currentThinkingBudget;
655
- if (budgetTokens2) queryOptions.thinking = { type: "enabled", budgetTokens: budgetTokens2 };
656
- }
657
-
658
- if (ls2.disableAllHooks !== undefined) {
659
- queryOptions.settings = Object.assign({}, queryOptions.settings || {}, { disableAllHooks: ls2.disableAllHooks });
660
- }
661
-
662
- if (dangerouslySkipPermissions) {
663
- queryOptions.allowDangerouslySkipPermissions = true;
664
- }
665
- var modeToApply = ls2.permissionMode || (session.acceptEditsAfterStart ? "acceptEdits" : sm.currentPermissionMode);
666
- if (session.acceptEditsAfterStart) delete session.acceptEditsAfterStart;
667
- if (modeToApply && modeToApply !== "default") {
668
- queryOptions.permissionMode = modeToApply;
669
- }
670
-
671
- if (session.cliSessionId) {
672
- queryOptions.resume = session.cliSessionId;
673
- if (session.lastRewindUuid) {
674
- queryOptions.resumeSessionAt = session.lastRewindUuid;
675
- delete session.lastRewindUuid;
676
- // Persist the deletion so server restarts don't re-use a stale UUID
677
- sm.saveSessionFile(session);
678
- }
679
- }
680
-
681
- // Set up message handler for worker events
682
- var firstEventLogged = false;
683
- worker.onMessage(function(msg) {
684
- if (!firstEventLogged && msg.type === "sdk_event") {
685
- firstEventLogged = true;
686
- perf("FIRST sdk_event received (type=" + (msg.event && msg.event.type || "?") + ")");
687
- }
688
- switch (msg.type) {
689
- case "sdk_event":
690
- processSDKMessage(session, msg.event);
691
- break;
692
-
693
- case "permission_request":
694
- handleCanUseTool(session, msg.toolName, msg.input, {
695
- toolUseID: msg.toolUseId,
696
- decisionReason: msg.decisionReason,
697
- signal: session.abortController ? { addEventListener: function() {} } : undefined,
698
- }).then(function(result) {
699
- worker.send({ type: "permission_response", requestId: msg.requestId, result: result });
700
- }).catch(function(e) {
701
- console.error("[sdk-bridge] permission_response send failed:", e.message || e);
702
- });
703
- break;
704
-
705
- case "ask_user_request":
706
- // Delegate to the daemon's AskUserQuestion handling
707
- handleCanUseTool(session, "AskUserQuestion", msg.input, {
708
- toolUseID: msg.toolUseId,
709
- signal: session.abortController ? { addEventListener: function() {} } : undefined,
710
- }).then(function(result) {
711
- worker.send({ type: "ask_user_response", toolUseId: msg.toolUseId, result: result });
712
- }).catch(function(e) {
713
- console.error("[sdk-bridge] ask_user_response send failed:", e.message || e);
714
- });
715
- break;
716
-
717
- case "elicitation_request":
718
- handleElicitation(session, {
719
- serverName: msg.serverName,
720
- message: msg.message,
721
- mode: msg.mode,
722
- url: msg.url,
723
- elicitationId: msg.elicitationId,
724
- requestedSchema: msg.requestedSchema,
725
- }, {
726
- signal: session.abortController ? { addEventListener: function() {} } : undefined,
727
- }).then(function(result) {
728
- worker.send({ type: "elicitation_response", requestId: msg.requestId, result: result });
729
- }).catch(function(e) {
730
- console.error("[sdk-bridge] elicitation_response send failed:", e.message || e);
731
- });
732
- break;
733
-
734
- case "mcp_tool_call":
735
- // Worker is proxying an MCP tool call back to the daemon where
736
- // the actual MCP server instances (with handlers) live.
737
- callMcpToolHandler(_mergedMcp, msg.serverName, msg.toolName, msg.args)
738
- .then(function(result) {
739
- worker.send({ type: "mcp_tool_result", requestId: msg.requestId, result: result });
740
- })
741
- .catch(function(e) {
742
- worker.send({ type: "mcp_tool_result", requestId: msg.requestId, error: e.message || String(e) });
743
- });
744
- break;
745
-
746
- case "context_usage":
747
- session.lastContextUsage = msg.data;
748
- sendToSession(session, { type: "context_usage", data: msg.data });
749
- break;
750
-
751
- case "query_done":
752
- console.log("[sdk-bridge] IPC query_done received, pid=" + (worker.process ? worker.process.pid : "?"));
753
- // Mark that we received a proper IPC completion, so the exit
754
- // handler fallback knows not to double-process.
755
- worker._queryEnded = true;
756
- // Stream ended normally
757
- if (session.isProcessing && session.taskStopRequested) {
758
- session.isProcessing = false;
759
- onProcessingChanged();
760
- sendAndRecord(session, { type: "info", text: "Interrupted \u00b7 What should Claude do instead?" });
761
- sendAndRecord(session, { type: "done", code: 0 });
762
- sm.broadcastSessionList();
763
- }
764
- cleanupSessionWorker(session, worker);
765
- // Mark session as done so late rate_limit_event can detect race condition
766
- session.isProcessing = false;
767
- // Auto-continue on rate limit (scheduler sessions, or user setting)
768
- var doneDidScheduleAC = false;
769
- var doneACEnabled = session.onQueryComplete || (typeof opts.getAutoContinueSetting === "function" && opts.getAutoContinueSetting(session));
770
- console.log("[sdk-bridge] query_done: session " + session.localId + " rateLimitResetsAt=" + session.rateLimitResetsAt + " acEnabled=" + doneACEnabled + " destroying=" + session.destroying + " scheduledMessage=" + !!session.scheduledMessage);
771
- if (session.rateLimitResetsAt && session.rateLimitResetsAt > Date.now()
772
- && doneACEnabled && !session.destroying) {
773
- var doneResetsAt = session.rateLimitResetsAt;
774
- session.rateLimitResetsAt = null;
775
- session.rateLimitAutoContinuePending = true;
776
- doneDidScheduleAC = true;
777
- console.log("[sdk-bridge] Rate limited (worker/query_done), scheduling auto-continue for session " + session.localId);
778
- if (typeof opts.scheduleMessage === "function") {
779
- opts.scheduleMessage(session, "continue", doneResetsAt);
780
- }
781
- }
782
- if (session.onQueryComplete && !doneDidScheduleAC) {
783
- try { session.onQueryComplete(session); } catch (err) {
784
- console.error("[sdk-bridge] onQueryComplete error:", err.message || err);
785
- }
786
- }
787
- break;
788
-
789
- case "query_error": {
790
- console.log("[sdk-bridge] IPC query_error received, pid=" + (worker.process ? worker.process.pid : "?") + " _fallback=" + !!msg._fallback + " _queryEnded=" + worker._queryEnded + " error=" + (msg.error || "").substring(0, 100));
791
- // Skip fallback errors from exit handler if we already handled the real one
792
- if (msg._fallback && worker._queryEnded) break;
793
- // Mark that we received a proper IPC completion
794
- worker._queryEnded = true;
795
- // Check session-not-found before isProcessing gate (it can arrive after processing is cleared)
796
- var qerrLower = (msg.error || "").toLowerCase();
797
- // Only match the exact SDK error, not generic worker stderr
798
- var isSessionNotFound = qerrLower.indexOf("no conversation found with session id") !== -1;
799
- if (isSessionNotFound) {
800
- // Clear stale cliSessionId so next message starts a fresh
801
- // conversation in the same UI session.
802
- session.cliSessionId = null;
803
- }
804
- if (session.isProcessing) {
805
- session.isProcessing = false;
806
- onProcessingChanged();
807
- var isAbort = (msg.error && (msg.error.indexOf("AbortError") !== -1 || msg.error.indexOf("aborted") !== -1))
808
- || session.taskStopRequested;
809
- if (isAbort) {
810
- if (!session.destroying) {
811
- sendAndRecord(session, { type: "info", text: "Interrupted \u00b7 What should Claude do instead?" });
812
- sendAndRecord(session, { type: "done", code: 0 });
813
- }
814
- } else if (session.destroying) {
815
- console.log("[sdk-bridge] Suppressing worker error during shutdown for session " + session.localId);
816
- } else {
817
- var errDetail = msg.error || "Unknown error";
818
- if (msg.stderr) errDetail += "\nstderr: " + msg.stderr;
819
- if (msg.exitCode != null) errDetail += " (exitCode: " + msg.exitCode + ")";
820
- console.error("[sdk-bridge] Worker query error for session " + session.localId + ":", errDetail);
821
-
822
- var errLower = errDetail.toLowerCase();
823
- var isContextOverflow = errLower.indexOf("prompt is too long") !== -1
824
- || errLower.indexOf("context_length") !== -1
825
- || errLower.indexOf("maximum context length") !== -1;
826
- var isAuthError = errLower.indexOf("not logged in") !== -1
827
- || errLower.indexOf("unauthenticated") !== -1
828
- || errLower.indexOf("authentication") !== -1
829
- || errLower.indexOf("sign in") !== -1
830
- || errLower.indexOf("log in") !== -1
831
- || errLower.indexOf("please login") !== -1;
832
- if (isContextOverflow) {
833
- sendAndRecord(session, { type: "context_overflow", text: "Conversation too long to continue." });
834
- } else if (isAuthError) {
835
- var authUser = session.ownerId ? usersModule.findUserById(session.ownerId) : null;
836
- var authLinuxUser = authUser && authUser.linuxUser ? authUser.linuxUser : null;
837
- // Determine if auto-login (auto terminal + claude) is safe:
838
- // - Single-user mode: always ok
839
- // - Multi-user + OS user isolation (linuxUser set): ok (isolated)
840
- // - Multi-user + admin role: ok (they own the shared account)
841
- // - Multi-user + regular user (no linuxUser): not ok (shared account)
842
- var canAutoLogin = !usersModule.isMultiUser()
843
- || !!authLinuxUser
844
- || (authUser && authUser.role === "admin");
845
- sendAndRecord(session, {
846
- type: "auth_required",
847
- text: "Claude Code is not logged in.",
848
- linuxUser: authLinuxUser,
849
- canAutoLogin: canAutoLogin,
850
- });
851
- } else {
852
- var errText = msg.error || "Unknown error";
853
- // When stderr is empty, fall back to worker stderr buffer (covers hook failures at session start)
854
- if (!msg.stderr && worker._stderrBuf) {
855
- errText += "\n" + worker._stderrBuf.trim();
856
- }
857
- sendAndRecord(session, { type: "error", text: "Claude process error: " + errText });
858
- }
859
- sendAndRecord(session, { type: "done", code: 1 });
860
- }
861
- sm.broadcastSessionList();
862
- }
863
- cleanupSessionWorker(session, worker);
864
- // Mark session as done so late rate_limit_event can detect race condition
865
- session.isProcessing = false;
866
- // Auto-continue on rate limit (scheduler sessions, or user setting)
867
- var workerDidScheduleAC = false;
868
- var workerACEnabled = session.onQueryComplete || (typeof opts.getAutoContinueSetting === "function" && opts.getAutoContinueSetting(session));
869
- if (session.rateLimitResetsAt && session.rateLimitResetsAt > Date.now()
870
- && workerACEnabled && !session.destroying) {
871
- var wacResetsAt = session.rateLimitResetsAt;
872
- session.rateLimitResetsAt = null;
873
- session.rateLimitAutoContinuePending = true;
874
- workerDidScheduleAC = true;
875
- console.log("[sdk-bridge] Rate limited (worker), scheduling auto-continue via scheduleMessage for session " + session.localId);
876
- if (typeof opts.scheduleMessage === "function") {
877
- opts.scheduleMessage(session, "continue", wacResetsAt);
259
+ var dirStat = fs.statSync(dstDir);
260
+ if (dirStat.uid !== uid) {
261
+ require("child_process").execSync("chown " + uid + " " + JSON.stringify(dstDir));
878
262
  }
879
- }
880
- if (session.onQueryComplete && !workerDidScheduleAC) {
881
- try { session.onQueryComplete(session); } catch (err) {
882
- console.error("[sdk-bridge] onQueryComplete error:", err.message || err);
883
- }
884
- }
885
- break;
263
+ } catch (e2) {}
886
264
  }
887
-
888
- case "model_changed":
889
- sm.currentModel = msg.model;
890
- send({ type: "model_info", model: msg.model, models: sm.availableModels || [] });
891
- send({ type: "config_state", model: sm.currentModel, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
892
- break;
893
-
894
- case "effort_changed":
895
- sm.currentEffort = msg.effort;
896
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [] });
897
- break;
898
-
899
- case "permission_mode_changed":
900
- sm.currentPermissionMode = msg.mode;
901
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
902
- break;
903
-
904
- case "worker_error":
905
- send({ type: "error", text: msg.error });
906
- break;
907
- }
908
- });
909
-
910
- // Wait for worker to be ready, then send query
911
- if (!reusingWorker) {
912
- perf("awaiting readyPromise");
913
- try {
914
- await worker.readyPromise;
915
- perf("readyPromise resolved");
916
- } catch (e) {
917
- session.isProcessing = false;
918
- onProcessingChanged();
919
- sendAndRecord(session, { type: "error", text: "Worker failed to connect: " + (e.message || e) });
920
- sendAndRecord(session, { type: "done", code: 1 });
921
- sm.broadcastSessionList();
922
- killSessionWorker(session);
923
- return;
924
- }
925
- }
926
-
927
- perf("sending query_start to worker");
928
- worker.send({
929
- type: "query_start",
930
- prompt: initialMessage,
931
- options: queryOptions,
932
- mcpDescriptors: _mcpDescriptors,
933
- singleTurn: !!session.singleTurn,
934
- originalHome: require("./config").REAL_HOME || null,
935
- projectPath: session.cwd || null,
936
- _perfT0: t0,
937
- });
938
- perf("query_start sent");
939
- }
940
-
941
- function cleanupSessionWorker(session, fromWorker) {
942
- console.log("[sdk-bridge] cleanupSessionWorker() called, localId=" + session.localId +
943
- " fromWorkerPid=" + (fromWorker && fromWorker.process ? fromWorker.process.pid : "none") +
944
- " currentWorkerPid=" + (session.worker && session.worker.process ? session.worker.process.pid : "none") +
945
- " stack=" + new Error().stack.split("\n").slice(1, 4).join(" | "));
946
- // If called from a specific worker's exit/error handler, only cleanup if
947
- // that worker is still the session's current worker. Prevents stale
948
- // worker exit events from killing a newer worker.
949
- if (fromWorker && session.worker && session.worker !== fromWorker) {
950
- console.log("[sdk-bridge] cleanupSessionWorker: stale worker guard triggered, skipping");
951
- return;
952
- }
953
- session.queryInstance = null;
954
- session.messageQueue = null;
955
- session.abortController = null;
956
- session.taskStopRequested = false;
957
- session.pendingPermissions = {};
958
- session.pendingAskUser = {};
959
- session.pendingElicitations = {};
960
- // Keep the worker alive between queries so the SDK can maintain session
961
- // state in memory. Killing the worker after each query forces resume from
962
- // disk, but the SDK may not save the session file on abort, causing
963
- // "no conversation found" and losing all conversation history.
964
- // The worker is only killed when the UI session is destroyed or on error.
965
- }
966
-
967
- // Force-kill the worker and remove it from the session.
968
- // Used when the session is destroyed or on unrecoverable errors.
969
- function killSessionWorker(session) {
970
- if (session.worker) {
971
- session._workerExitPromise = session.worker.exitPromise;
972
- session.worker.kill();
973
- session.worker = null;
974
- }
975
- }
976
-
977
- /**
978
- * Run warmup via a worker process for a specific Linux user.
979
- */
980
- async function warmupViaWorker(linuxUser) {
981
- var worker;
982
- try {
983
- worker = spawnWorker(linuxUser);
984
- } catch (e) {
985
- send({ type: "error", text: "Failed to spawn warmup worker for " + linuxUser + ": " + (e.message || e) });
986
- return;
987
- }
988
-
989
- var warmupDone = false;
990
-
991
- worker.onMessage(function(msg) {
992
- if (msg.type === "warmup_done" && !warmupDone) {
993
- warmupDone = true;
994
- var result = msg.result || {};
995
- var fsSkills = discoverSkillDirs();
996
- sm.skillNames = mergeSkills(result.skills, fsSkills);
997
- if (result.slashCommands) {
998
- var seen = new Set();
999
- var combined = [];
1000
- var all = result.slashCommands.concat(Array.from(sm.skillNames));
1001
- for (var k = 0; k < all.length; k++) {
1002
- if (!seen.has(all[k])) {
1003
- seen.add(all[k]);
1004
- combined.push(all[k]);
1005
- }
265
+ // Pre-copy CLI session file so the worker can resume the conversation
266
+ if (session.cliSessionId) {
267
+ var sessionFileName = session.cliSessionId + ".jsonl";
268
+ var srcFile = path.join(originalHome, ".claude", "projects", projectSlug, sessionFileName);
269
+ var dstFile = path.join(dstDir, sessionFileName);
270
+ if (fs.existsSync(srcFile) && !fs.existsSync(dstFile)) {
271
+ fs.copyFileSync(srcFile, dstFile);
272
+ try { require("child_process").execSync("chown " + uid + " " + JSON.stringify(dstFile)); } catch (e2) {}
273
+ console.log("[sdk-bridge] Pre-copied CLI session " + session.cliSessionId + " to " + linuxUser);
1006
274
  }
1007
- sm.slashCommands = combined;
1008
- send({ type: "slash_commands", commands: sm.slashCommands });
1009
- }
1010
- if (result.model) {
1011
- sm.currentModel = sm._savedDefaultModel || result.model;
1012
275
  }
1013
- sm.availableModels = result.models || [];
1014
- send({ type: "model_info", model: sm.currentModel || "", models: sm.availableModels || [] });
1015
- worker.kill();
1016
- } else if (msg.type === "warmup_error" && !warmupDone) {
1017
- warmupDone = true;
1018
- send({ type: "error", text: msg.error || "Warmup failed" });
1019
- worker.kill();
1020
276
  }
1021
- });
1022
-
1023
- try {
1024
- await worker.readyPromise;
1025
- } catch (e) {
1026
- send({ type: "error", text: "Warmup worker failed to connect: " + (e.message || e) });
1027
- cleanupWorker(worker);
1028
- return;
277
+ } catch (copyErr) {
278
+ console.log("[sdk-bridge] Dir setup / session pre-copy skipped:", copyErr.message);
1029
279
  }
1030
-
1031
- var warmupOptions = { cwd: cwd, settingSources: ["user", "project", "local"], settings: { disableAllHooks: true } };
1032
- if (dangerouslySkipPermissions) {
1033
- warmupOptions.permissionMode = "bypassPermissions";
1034
- warmupOptions.allowDangerouslySkipPermissions = true;
1035
- }
1036
- worker.send({ type: "warmup", options: warmupOptions });
1037
280
  }
1038
281
 
1039
282
  // --- SDK query lifecycle ---
@@ -1235,6 +478,7 @@ function createSDKBridge(opts) {
1235
478
  toolInput: input,
1236
479
  toolUseId: opts.toolUseID,
1237
480
  decisionReason: opts.decisionReason || "",
481
+ vendor: session.vendor || (adapter && adapter.vendor) || "claude",
1238
482
  };
1239
483
  sendAndRecord(session, permMsg);
1240
484
  onProcessingChanged(); // update cross-project permission badge
@@ -1338,19 +582,53 @@ function createSDKBridge(opts) {
1338
582
  // Capture references at start so we only clean up OUR resources in finally,
1339
583
  // not resources from a newer query that may have been created after an abort.
1340
584
  var myQueryInstance = session.queryInstance;
1341
- var myMessageQueue = session.messageQueue;
1342
585
  var myAbortController = session.abortController;
586
+ console.log("[sdk-bridge] processQueryStream: starting for-await loop, vendor=" + (session.vendor || adapter.vendor));
1343
587
  try {
1344
588
  for await (var msg of myQueryInstance) {
589
+ console.log("[sdk-bridge] processQueryStream: received event yokeType=" + (msg && msg.yokeType) + " type=" + (msg && msg.type) + (msg && msg.text ? " text=" + msg.text.substring(0, 200) : ""));
590
+ // Handle worker meta events (context_usage, model_changed, etc.)
591
+ if (msg && msg.type === "_worker_meta") {
592
+ var metaData = msg.data || {};
593
+ switch (msg.subtype) {
594
+ case "context_usage":
595
+ session.lastContextUsage = metaData.data;
596
+ sendToSession(session, { type: "context_usage", data: metaData.data });
597
+ break;
598
+ case "model_changed":
599
+ sm.currentModel = metaData.model;
600
+ send({ type: "model_info", model: metaData.model, models: sm.availableModels || [], vendor: adapter.vendor });
601
+ send({ type: "config_state", model: sm.currentModel, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
602
+ break;
603
+ case "effort_changed":
604
+ sm.currentEffort = metaData.effort;
605
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [] });
606
+ break;
607
+ case "permission_mode_changed":
608
+ sm.currentPermissionMode = metaData.mode;
609
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
610
+ break;
611
+ case "worker_error":
612
+ send({ type: "error", text: metaData.error });
613
+ break;
614
+ }
615
+ continue;
616
+ }
1345
617
  processSDKMessage(session, msg);
1346
618
  }
1347
619
  // (getContextUsage moved to processSDKMessage result handler -- fire-and-forget)
1348
620
  // Stream ended normally after a task stop — no "result" message was sent,
1349
621
  // so the session is still marked as processing. Send interrupted feedback.
622
+ console.log("[sdk-bridge] processQueryStream ended: isProcessing=" + session.isProcessing + " taskStopRequested=" + session.taskStopRequested);
1350
623
  if (session.isProcessing && session.taskStopRequested) {
1351
624
  session.isProcessing = false;
1352
625
  onProcessingChanged();
1353
- sendAndRecord(session, { type: "info", text: "Interrupted \u00b7 What should Claude do instead?" });
626
+ send({ type: "status", processing: false });
627
+ sendAndRecord(session, { type: "thinking_stop" });
628
+ var interruptMsg = (session.vendor === "codex")
629
+ ? "\u25a0 Conversation interrupted - tell the model what to do differently."
630
+ : "Interrupted \u00b7 What should Claude do instead?";
631
+ sendAndRecord(session, { type: "info", text: interruptMsg });
1354
632
  sendAndRecord(session, { type: "done", code: 0 });
1355
633
  sm.broadcastSessionList();
1356
634
  }
@@ -1360,7 +638,11 @@ function createSDKBridge(opts) {
1360
638
  onProcessingChanged();
1361
639
  if (err.name === "AbortError" || (myAbortController && myAbortController.signal.aborted) || session.taskStopRequested) {
1362
640
  if (!session.destroying) {
1363
- sendAndRecord(session, { type: "info", text: "Interrupted \u00b7 What should Claude do instead?" });
641
+ sendAndRecord(session, { type: "thinking_stop" });
642
+ var interruptMsg2 = (session.vendor === "codex")
643
+ ? "\u25a0 Conversation interrupted - tell the model what to do differently."
644
+ : "Interrupted \u00b7 What should Claude do instead?";
645
+ sendAndRecord(session, { type: "info", text: interruptMsg2 });
1364
646
  sendAndRecord(session, { type: "done", code: 0 });
1365
647
  }
1366
648
  } else if (session.destroying) {
@@ -1433,7 +715,7 @@ function createSDKBridge(opts) {
1433
715
  } catch (e) {}
1434
716
  session.queryInstance = null;
1435
717
  }
1436
- if (session.messageQueue === myMessageQueue) session.messageQueue = null;
718
+ session.messageQueue = null;
1437
719
  if (session.abortController === myAbortController) session.abortController = null;
1438
720
  session.taskStopRequested = false;
1439
721
  session.pendingPermissions = {};
@@ -1478,58 +760,157 @@ function createSDKBridge(opts) {
1478
760
  async function getOrCreateRewindQuery(session) {
1479
761
  if (session.queryInstance) return { query: session.queryInstance, isTemp: false, cleanup: function() {} };
1480
762
 
1481
- var sdk;
763
+ var handle;
1482
764
  try {
1483
- sdk = await getSDK();
765
+ handle = await adapter.createQuery({
766
+ cwd: cwd,
767
+ resumeSessionId: session.cliSessionId,
768
+ adapterOptions: {
769
+ CLAUDE: {
770
+ settingSources: ["user", "project", "local"],
771
+ enableFileCheckpointing: true,
772
+ },
773
+ },
774
+ });
1484
775
  } catch (e) {
1485
776
  sendAndRecord(session, { type: "error", text: "Failed to load Claude SDK: " + (e.message || e) });
1486
777
  throw e;
1487
778
  }
1488
- var mq = createMessageQueue();
1489
-
1490
- var tempQuery = sdk.query({
1491
- prompt: mq,
1492
- options: {
1493
- cwd: cwd,
1494
- settingSources: ["user", "project", "local"],
1495
- enableFileCheckpointing: true,
1496
- resume: session.cliSessionId,
1497
- },
1498
- });
1499
779
 
1500
- // Drain messages in background (stream stays alive until mq.end())
780
+ // Drain messages in background (stream stays alive until close)
1501
781
  (async function() {
1502
- try { for await (var msg of tempQuery) {} } catch(e) {}
782
+ try { for await (var msg of handle) {} } catch(e) {}
1503
783
  })();
1504
784
 
1505
785
  return {
1506
- query: tempQuery,
786
+ query: handle,
1507
787
  isTemp: true,
1508
- cleanup: function() { try { mq.end(); } catch(e) {} },
788
+ cleanup: function() { try { handle.close(); } catch(e) {} },
1509
789
  };
1510
790
  }
1511
791
 
1512
- async function startQuery(session, text, images, linuxUser) {
1513
- // Remember linuxUser for auto-continue after rate limit
1514
- session.lastLinuxUser = linuxUser || null;
1515
- // OS-level isolation: delegate to worker process if linuxUser is set
1516
- if (linuxUser) {
1517
- return startQueryViaWorker(session, text, images, linuxUser);
792
+ // --- Unified rewind/fork interface (adapter-agnostic) ---
793
+
794
+ async function rewindPreview(session, uuid) {
795
+ var sessionAdapter = getAdapterForSession(session);
796
+ // Adapters with rollbackThread (e.g. Codex) do chat-only rewind, no file diffs
797
+ if (sessionAdapter && typeof sessionAdapter.rollbackThread === "function") {
798
+ return { preview: { filesChanged: [] }, diffs: {}, chatOnly: true };
799
+ }
800
+ // Claude path: use rewindFiles with dryRun
801
+ var result = await getOrCreateRewindQuery(session);
802
+ try {
803
+ var preview = await result.query.rewindFiles(uuid, { dryRun: true });
804
+ var diffs = {};
805
+ var changedFiles = preview.filesChanged || [];
806
+ for (var f = 0; f < changedFiles.length; f++) {
807
+ try {
808
+ diffs[changedFiles[f]] = require("child_process").execFileSync(
809
+ "git", ["diff", "HEAD", "--", changedFiles[f]],
810
+ { cwd: cwd, encoding: "utf8", timeout: 5000 }
811
+ ) || "";
812
+ } catch (e) { diffs[changedFiles[f]] = ""; }
813
+ }
814
+ return { preview: preview, diffs: diffs, chatOnly: false };
815
+ } finally {
816
+ if (result.isTemp) result.cleanup();
1518
817
  }
818
+ }
1519
819
 
1520
- var sdk;
820
+ async function rewindExecuteFiles(session, uuid) {
821
+ var sessionAdapter = getAdapterForSession(session);
822
+ // Adapters with rollbackThread skip file restoration
823
+ if (sessionAdapter && typeof sessionAdapter.rollbackThread === "function") return;
824
+ // Claude path: restore files
825
+ var result = await getOrCreateRewindQuery(session);
1521
826
  try {
1522
- sdk = await getSDK();
1523
- } catch (e) {
1524
- session.isProcessing = false;
1525
- onProcessingChanged();
1526
- sendAndRecord(session, { type: "error", text: "Failed to load Claude SDK: " + (e.message || e) });
827
+ await result.query.rewindFiles(uuid, { dryRun: false });
828
+ } finally {
829
+ if (result.isTemp) result.cleanup();
830
+ }
831
+ }
832
+
833
+ async function rollbackConversation(session, numTurns) {
834
+ var sessionAdapter = getAdapterForSession(session);
835
+ if (sessionAdapter && typeof sessionAdapter.rollbackThread === "function") {
836
+ await sessionAdapter.rollbackThread(session.cliSessionId, numTurns);
837
+ }
838
+ // Claude: conversation rollback is handled by rewindFiles + local history trim
839
+ }
840
+
841
+ function getAdapterForSession(session) {
842
+ var vendor = session.vendor || sm.defaultVendor || "claude";
843
+ return adapters[vendor] || adapter;
844
+ }
845
+
846
+ async function forkSessionUnified(session, uuid) {
847
+ var sessionAdapter = getAdapterForSession(session);
848
+ var result = await sessionAdapter.forkSession(session.cliSessionId, { upToMessageId: uuid, dir: cwd });
849
+ if (!result || !result.sessionId) throw new Error("Fork returned no session id");
850
+
851
+ // Adapters with rollbackThread (e.g. Codex) use local history copy
852
+ if (typeof sessionAdapter.rollbackThread === "function") {
853
+ return { sessionId: result.sessionId, useLocalHistory: true };
854
+ }
855
+ // Claude: read history from CLI session files
856
+ return { sessionId: result.sessionId, useLocalHistory: false };
857
+ }
858
+
859
+ async function startQuery(session, text, images, linuxUser) {
860
+ // If vendor is set but adapter not ready, try lazy creation (user may have logged in)
861
+ if (session.vendor && !adapters[session.vendor]) {
862
+ var yoke = require("./yoke");
863
+ var lazyAdapter = yoke.lazyCreateAdapter(adapters, session.vendor, { cwd: cwd });
864
+ if (lazyAdapter) {
865
+ console.log("[sdk-bridge] Lazy adapter created for " + session.vendor);
866
+ }
867
+ }
868
+ // If still not available after lazy check, send auth_required
869
+ if (session.vendor && !adapters[session.vendor]) {
870
+ var vendorName = session.vendor.charAt(0).toUpperCase() + session.vendor.slice(1);
871
+ var authUser = session.ownerId ? usersModule.findUserById(session.ownerId) : null;
872
+ var authLinuxUser = authUser && authUser.linuxUser ? authUser.linuxUser : null;
873
+ var canAutoLogin = !usersModule.isMultiUser()
874
+ || !!authLinuxUser
875
+ || (authUser && authUser.role === "admin");
876
+ sendAndRecord(session, {
877
+ type: "auth_required",
878
+ text: vendorName + " is not logged in.",
879
+ vendor: session.vendor,
880
+ loginCommand: session.vendor === "codex" ? "codex --login" : session.vendor + " login",
881
+ linuxUser: authLinuxUser,
882
+ canAutoLogin: canAutoLogin,
883
+ });
1527
884
  sendAndRecord(session, { type: "done", code: 1 });
1528
- sm.broadcastSessionList();
1529
885
  return;
1530
886
  }
887
+ // Select adapter based on session vendor (fallback to default)
888
+ var sessionAdapter = (session.vendor && adapters[session.vendor]) || adapter;
889
+ console.log("[sdk-bridge] startQuery: vendor=" + sessionAdapter.vendor + " session=" + session.localId + " text=" + (text || "").substring(0, 50));
890
+ // Remember linuxUser for auto-continue after rate limit
891
+ session.lastLinuxUser = linuxUser || null;
892
+
893
+ var t0 = session._queryStartTs || Date.now();
894
+
895
+ // Wait for previous worker to fully exit before spawning a new one.
896
+ // Without this, the new worker may try to resume the SDK session file
897
+ // while the old worker is still flushing it to disk, causing
898
+ // "no conversation found" and losing all prior context.
899
+ // Harmless if null (no previous worker).
900
+ if (session._workerExitPromise) {
901
+ var exitWait = session._workerExitPromise;
902
+ session._workerExitPromise = null;
903
+ await Promise.race([
904
+ exitWait,
905
+ new Promise(function(resolve) { setTimeout(resolve, 3000); }),
906
+ ]);
907
+ }
908
+
909
+ // Ensure Linux user project directory exists (runs in parallel with worker boot)
910
+ if (linuxUser) {
911
+ ensureLinuxUserProjectDir(linuxUser, session);
912
+ }
1531
913
 
1532
- session.messageQueue = createMessageQueue();
1533
914
  session.blocks = {};
1534
915
  session.sentToolResults = {};
1535
916
  session.activeTaskToolIds = {};
@@ -1537,106 +918,117 @@ function createSDKBridge(opts) {
1537
918
  session.streamedText = false;
1538
919
  session.responsePreview = "";
1539
920
 
1540
- // Build initial user message
1541
- var content = [];
1542
- if (images && images.length > 0) {
1543
- for (var i = 0; i < images.length; i++) {
1544
- content.push({
1545
- type: "image",
1546
- source: { type: "base64", media_type: images[i].mediaType, data: images[i].data },
1547
- });
1548
- }
921
+ // For in-process path, create AbortController. For worker path, the adapter
922
+ // handles abort internally and exposes it via handle.abort().
923
+ if (!linuxUser) {
924
+ session.abortController = new AbortController();
1549
925
  }
1550
- if (text) {
1551
- content.push({ type: "text", text: text });
1552
- }
1553
-
1554
- session.messageQueue.push({
1555
- type: "user",
1556
- message: { role: "user", content: content },
1557
- });
1558
926
 
1559
- session.abortController = new AbortController();
1560
-
1561
- var queryOptions = {
1562
- cwd: cwd,
927
+ // Build Claude-specific adapter options
928
+ var claudeOpts = {
1563
929
  settingSources: ["user", "project", "local"],
1564
930
  includePartialMessages: true,
1565
931
  enableFileCheckpointing: true,
1566
932
  extraArgs: { "replay-user-messages": null },
1567
- abortController: session.abortController,
1568
933
  promptSuggestions: true,
1569
934
  agentProgressSummaries: true,
1570
- mcpServers: mergeMcpServers(mcpServers, getRemoteMcpServers) || undefined,
1571
- canUseTool: function(toolName, input, toolOpts) {
1572
- return handleCanUseTool(session, toolName, input, toolOpts);
1573
- },
1574
- onElicitation: function(request, elicitOpts) {
1575
- return handleElicitation(session, request, elicitOpts);
1576
- },
1577
935
  };
1578
936
 
1579
937
  // Per-loop settings override global defaults when present
1580
938
  var ls = session.loopSettings || {};
1581
939
 
1582
- if (ls.model || sm.currentModel) {
1583
- queryOptions.model = ls.model || sm.currentModel;
1584
- }
1585
-
1586
- if (ls.effort || sm.currentEffort) {
1587
- queryOptions.effort = ls.effort || sm.currentEffort;
1588
- }
1589
-
1590
940
  if (sm.currentBetas && sm.currentBetas.length > 0) {
1591
- queryOptions.betas = sm.currentBetas;
941
+ claudeOpts.betas = sm.currentBetas;
1592
942
  }
1593
-
1594
943
  var thinkingMode = ls.thinking || sm.currentThinking;
1595
944
  if (thinkingMode === "disabled") {
1596
- queryOptions.thinking = { type: "disabled" };
945
+ claudeOpts.thinking = { type: "disabled" };
1597
946
  } else if (thinkingMode === "budget") {
1598
947
  var budgetTokens = ls.thinkingBudget || sm.currentThinkingBudget;
1599
- if (budgetTokens) queryOptions.thinking = { type: "enabled", budgetTokens: budgetTokens };
948
+ if (budgetTokens) claudeOpts.thinking = { type: "enabled", budgetTokens: budgetTokens };
1600
949
  }
1601
950
 
1602
951
  if (ls.permissionMode) {
1603
- // Will be applied below, store for later
1604
952
  session._loopPermissionMode = ls.permissionMode;
1605
953
  }
1606
954
 
1607
955
  // Pass through any extra SDK settings from LOOP.json
1608
956
  if (ls.disableAllHooks !== undefined) {
1609
- queryOptions.settings = Object.assign({}, queryOptions.settings || {}, { disableAllHooks: ls.disableAllHooks });
957
+ claudeOpts.settings = Object.assign({}, claudeOpts.settings || {}, { disableAllHooks: ls.disableAllHooks });
1610
958
  }
1611
959
 
1612
960
  if (dangerouslySkipPermissions) {
1613
- queryOptions.allowDangerouslySkipPermissions = true;
961
+ claudeOpts.allowDangerouslySkipPermissions = true;
1614
962
  }
1615
- // Pass permissionMode in queryOptions at creation time to avoid race condition
1616
963
  var modeToApply = session._loopPermissionMode || (session.acceptEditsAfterStart ? "acceptEdits" : sm.currentPermissionMode);
1617
964
  if (session.acceptEditsAfterStart) delete session.acceptEditsAfterStart;
1618
965
  if (modeToApply && modeToApply !== "default") {
1619
- queryOptions.permissionMode = modeToApply;
966
+ claudeOpts.permissionMode = modeToApply;
967
+ }
968
+ if (session.cliSessionId && session.lastRewindUuid) {
969
+ claudeOpts.resumeSessionAt = session.lastRewindUuid;
970
+ delete session.lastRewindUuid;
971
+ sm.saveSessionFile(session);
972
+ }
973
+
974
+ // Pass linuxUser to adapter for worker-based queries
975
+ if (linuxUser) {
976
+ claudeOpts.linuxUser = linuxUser;
977
+ claudeOpts.singleTurn = !!session.singleTurn;
978
+ claudeOpts.originalHome = require("./config").REAL_HOME || null;
979
+ claudeOpts.projectPath = session.cwd || null;
980
+ claudeOpts._perfT0 = t0;
981
+ // Pass previous worker state for reuse
982
+ if (session._adapterWorkerState) {
983
+ claudeOpts._workerState = session._adapterWorkerState;
984
+ session._adapterWorkerState = null;
985
+ }
1620
986
  }
1621
987
 
1622
- if (session.cliSessionId) {
1623
- queryOptions.resume = session.cliSessionId;
1624
- if (session.lastRewindUuid) {
1625
- queryOptions.resumeSessionAt = session.lastRewindUuid;
1626
- delete session.lastRewindUuid;
1627
- // Persist the deletion so server restarts don't re-use a stale UUID
1628
- sm.saveSessionFile(session);
988
+ // Use vendor-specific model: if session vendor differs from default, use that vendor's default model
989
+ var queryModel = ls.model || sm.currentModel || undefined;
990
+ if (session.vendor && session.vendor !== (adapter && adapter.vendor)) {
991
+ var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[session.vendor]) || [];
992
+ if (vendorModels.length > 0 && queryModel && vendorModels.indexOf(queryModel) === -1) {
993
+ queryModel = vendorModels[0];
1629
994
  }
1630
995
  }
1631
996
 
997
+ var queryOpts = {
998
+ cwd: cwd,
999
+ model: queryModel,
1000
+ effort: ls.effort || sm.currentEffort || undefined,
1001
+ toolServers: mergeMcpServers(mcpServers, getRemoteMcpServers) || undefined,
1002
+ resumeSessionId: session.cliSessionId || undefined,
1003
+ abortController: linuxUser ? undefined : session.abortController,
1004
+ canUseTool: function(toolName, input, toolOpts) {
1005
+ return handleCanUseTool(session, toolName, input, toolOpts);
1006
+ },
1007
+ onElicitation: function(request, elicitOpts) {
1008
+ return handleElicitation(session, request, elicitOpts);
1009
+ },
1010
+ adapterOptions: {
1011
+ CLAUDE: claudeOpts,
1012
+ CODEX: {
1013
+ // Always use "never" (auto-approve) because Clay handles tool
1014
+ // permissions via its own UI (checkToolWhitelist + handleCanUseTool).
1015
+ // Codex's native approval prompts are terminal-based and cannot be
1016
+ // relayed through Clay's web UI, causing MCP tool calls to hang.
1017
+ approvalPolicy: "never",
1018
+ sandboxMode: sm.codexSandbox || "workspace-write",
1019
+ webSearchMode: sm.codexWebSearch || undefined,
1020
+ },
1021
+ },
1022
+ };
1023
+
1024
+ var handle;
1025
+ console.log("[sdk-bridge] calling adapter.createQuery... vendor=" + sessionAdapter.vendor);
1632
1026
  try {
1633
- session.queryInstance = sdk.query({
1634
- prompt: session.messageQueue,
1635
- options: queryOptions,
1636
- });
1027
+ handle = await sessionAdapter.createQuery(queryOpts);
1028
+ console.log("[sdk-bridge] createQuery returned handle, vendor=" + sessionAdapter.vendor);
1637
1029
  } catch (e) {
1638
1030
  console.error("[sdk-bridge] Failed to create query for session " + session.localId + ":", e.message || e);
1639
- console.error("[sdk-bridge] cliSessionId:", session.cliSessionId, "resume:", !!queryOptions.resume);
1031
+ console.error("[sdk-bridge] cliSessionId:", session.cliSessionId, "resume:", !!session.cliSessionId);
1640
1032
  console.error("[sdk-bridge] Stack:", e.stack || "(no stack)");
1641
1033
  session.isProcessing = false;
1642
1034
  onProcessingChanged();
@@ -1649,12 +1041,38 @@ function createSDKBridge(opts) {
1649
1041
  return;
1650
1042
  }
1651
1043
 
1044
+ // Store adapter worker state for reuse on next query
1045
+ if (handle._adapterState) {
1046
+ session._adapterWorkerState = handle._adapterState;
1047
+ // Keep session.worker reference for external code (sessions.js, project.js)
1048
+ // that needs to kill the worker on session destroy.
1049
+ if (handle._adapterState.worker) {
1050
+ session.worker = handle._adapterState.worker;
1051
+ }
1052
+ }
1053
+
1054
+ // For worker path, create an abortController wrapper that delegates to handle.abort()
1055
+ if (linuxUser) {
1056
+ session.abortController = {
1057
+ abort: function() { handle.abort(); },
1058
+ signal: { aborted: false, addEventListener: function() {} },
1059
+ };
1060
+ }
1061
+
1062
+ // Store QueryHandle on session for iteration and control.
1063
+ session.queryInstance = handle;
1064
+
1065
+ // Push initial user message through the QueryHandle
1066
+ console.log("[sdk-bridge] pushing initial message via handle.pushMessage...");
1067
+ handle.pushMessage(text, images);
1068
+ console.log("[sdk-bridge] pushMessage done, starting processQueryStream...");
1069
+
1652
1070
  // For single-turn sessions (Ralph Loop), end the message queue so the SDK
1653
1071
  // query finishes after processing the one message. Without this, the query
1654
1072
  // stream stays open forever waiting for more messages, and onQueryComplete
1655
1073
  // never fires.
1656
1074
  if (session.singleTurn) {
1657
- session.messageQueue.end();
1075
+ handle.endInput();
1658
1076
  }
1659
1077
 
1660
1078
  session.lastActivityAt = Date.now();
@@ -1663,28 +1081,10 @@ function createSDKBridge(opts) {
1663
1081
  }
1664
1082
 
1665
1083
  function pushMessage(session, text, images) {
1666
- var content = [];
1667
- if (images && images.length > 0) {
1668
- for (var i = 0; i < images.length; i++) {
1669
- content.push({
1670
- type: "image",
1671
- source: { type: "base64", media_type: images[i].mediaType, data: images[i].data },
1672
- });
1673
- }
1674
- }
1675
- if (text) {
1676
- content.push({ type: "text", text: text });
1677
- }
1678
- var userMsg = {
1679
- type: "user",
1680
- message: { role: "user", content: content },
1681
- };
1682
1084
  session.lastActivityAt = Date.now();
1683
- // Route through worker if active, otherwise direct to message queue
1684
- if (session.worker) {
1685
- session.worker.send({ type: "push_message", content: userMsg });
1686
- } else {
1687
- session.messageQueue.push(userMsg);
1085
+ // Route through QueryHandle (works for both in-process and worker paths)
1086
+ if (session.queryInstance && typeof session.queryInstance.pushMessage === "function") {
1087
+ session.queryInstance.pushMessage(text, images);
1688
1088
  }
1689
1089
  }
1690
1090
 
@@ -1731,74 +1131,124 @@ function createSDKBridge(opts) {
1731
1131
  return text;
1732
1132
  }
1733
1133
 
1734
- // SDK warmup: grab slash_commands, model, and available models from SDK init
1735
- async function warmup(linuxUser) {
1736
- // OS-level isolation: delegate warmup to worker process
1737
- if (linuxUser) {
1738
- return warmupViaWorker(linuxUser);
1739
- }
1134
+ // Detect which vendor binaries are installed for this user.
1135
+ // In multi-user mode, runs checks as the specific Linux user.
1136
+ function detectInstalledVendors(linuxUser) {
1137
+ var execSync = require("child_process").execSync;
1138
+ var fs = require("fs");
1139
+ var path = require("path");
1140
+ var result = [];
1740
1141
 
1741
- try {
1742
- var sdk = await getSDK();
1743
- var ac = new AbortController();
1744
- var mq = createMessageQueue();
1745
- mq.push({ type: "user", message: { role: "user", content: [{ type: "text", text: "hi" }] } });
1746
- mq.end();
1747
- var warmupOptions = { cwd: cwd, settingSources: ["user", "project", "local"], abortController: ac, settings: { disableAllHooks: true } };
1748
- if (dangerouslySkipPermissions) {
1749
- warmupOptions.permissionMode = "bypassPermissions";
1750
- warmupOptions.allowDangerouslySkipPermissions = true;
1142
+ function tryExec(cmd) {
1143
+ try {
1144
+ if (linuxUser) {
1145
+ execSync("su - " + linuxUser + " -c " + JSON.stringify(cmd), { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
1146
+ } else {
1147
+ execSync(cmd, { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
1148
+ }
1149
+ return true;
1150
+ } catch (e) {
1151
+ return false;
1751
1152
  }
1752
- var stream = sdk.query({
1753
- prompt: mq,
1754
- options: warmupOptions,
1755
- });
1756
- for await (var msg of stream) {
1757
- if (msg.type === "system" && msg.subtype === "init") {
1758
- var fsSkills = discoverSkillDirs();
1759
- sm.skillNames = mergeSkills(msg.skills, fsSkills);
1760
- if (msg.slash_commands) {
1761
- // Union: SDK slash_commands + merged skills (deduplicated)
1762
- var seen = new Set();
1763
- var combined = [];
1764
- var all = msg.slash_commands.concat(Array.from(sm.skillNames));
1765
- for (var k = 0; k < all.length; k++) {
1766
- if (!seen.has(all[k])) {
1767
- seen.add(all[k]);
1768
- combined.push(all[k]);
1769
- }
1153
+ }
1154
+
1155
+ // Claude: check if binary is in PATH
1156
+ if (tryExec("which claude")) result.push("claude");
1157
+
1158
+ // Codex: check bundled binary or PATH
1159
+ var codexBin = path.join(__dirname, "../node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/codex/codex");
1160
+ if (fs.existsSync(codexBin) || tryExec("which codex")) result.push("codex");
1161
+
1162
+ return result;
1163
+ }
1164
+
1165
+ // SDK warmup: initialize all available adapters and collect models.
1166
+ // The default adapter is initialized first for slash_commands and skills.
1167
+ // Passes linuxUser to adapter for worker-based warmup when OS isolation is needed.
1168
+ async function warmup(linuxUser) {
1169
+ var defaultVendor = adapter ? adapter.vendor : "claude";
1170
+ sm.defaultVendor = defaultVendor;
1171
+
1172
+ // Initialize default adapter first (provides skills, slash commands, etc.)
1173
+ if (adapter) {
1174
+ try {
1175
+ var result = await adapter.init({
1176
+ cwd: cwd,
1177
+ dangerouslySkipPermissions: dangerouslySkipPermissions,
1178
+ linuxUser: linuxUser || undefined,
1179
+ clayPort: clayPort,
1180
+ clayTls: clayTls,
1181
+ clayAuthToken: clayAuthToken,
1182
+ slug: slug,
1183
+ });
1184
+
1185
+ var fsSkills = discoverSkillDirs();
1186
+ sm.skillNames = mergeSkills(result.skills, fsSkills);
1187
+ if (result.slashCommands) {
1188
+ var seen = new Set();
1189
+ var combined = [];
1190
+ var all = result.slashCommands.concat(Array.from(sm.skillNames));
1191
+ for (var k = 0; k < all.length; k++) {
1192
+ if (!seen.has(all[k])) {
1193
+ seen.add(all[k]);
1194
+ combined.push(all[k]);
1770
1195
  }
1771
- sm.slashCommands = combined;
1772
- send({ type: "slash_commands", commands: sm.slashCommands });
1773
1196
  }
1774
- if (msg.model) {
1775
- sm.currentModel = msg.model;
1776
- }
1777
- // Fetch available models before aborting
1778
- try {
1779
- var models = await stream.supportedModels();
1780
- sm.availableModels = models || [];
1781
- } catch (e) {}
1782
- send({ type: "model_info", model: sm.currentModel || "", models: sm.availableModels || [] });
1783
- ac.abort();
1784
- break;
1197
+ sm.slashCommands = combined;
1198
+ send({ type: "slash_commands", commands: sm.slashCommands });
1199
+ }
1200
+ if (result.defaultModel) {
1201
+ sm.currentModel = sm._savedDefaultModel || result.defaultModel;
1202
+ }
1203
+ sm.availableModels = result.models || [];
1204
+ // Store per-vendor models and capabilities
1205
+ sm.modelsByVendor = sm.modelsByVendor || {};
1206
+ sm.modelsByVendor[defaultVendor] = result.models || [];
1207
+ sm.capabilitiesByVendor = sm.capabilitiesByVendor || {};
1208
+ sm.capabilitiesByVendor[defaultVendor] = result.capabilities || {};
1209
+ } catch (e) {
1210
+ if (e && e.name !== "AbortError" && !(e.message && e.message.indexOf("aborted") !== -1)) {
1211
+ send({ type: "error", text: "Failed to load " + defaultVendor + " SDK: " + (e.message || e) });
1785
1212
  }
1786
- }
1787
- } catch (e) {
1788
- if (e && e.name !== "AbortError" && !(e.message && e.message.indexOf("aborted") !== -1)) {
1789
- send({ type: "error", text: "Failed to load Claude SDK: " + (e.message || e) });
1790
1213
  }
1791
1214
  }
1215
+
1216
+ // Initialize other adapters in parallel (skip if already have models cached)
1217
+ sm.modelsByVendor = sm.modelsByVendor || {};
1218
+ var otherVendors = Object.keys(adapters).filter(function(v) {
1219
+ return v !== defaultVendor && !sm.modelsByVendor[v];
1220
+ });
1221
+ for (var i = 0; i < otherVendors.length; i++) {
1222
+ (function(v) {
1223
+ adapters[v].init({ cwd: cwd, clayPort: clayPort, clayTls: clayTls, clayAuthToken: clayAuthToken, slug: slug }).then(function(r) {
1224
+ sm.modelsByVendor[v] = r.models || [];
1225
+ sm.capabilitiesByVendor[v] = r.capabilities || {};
1226
+ }).catch(function(e) {
1227
+ console.error("[sdk-bridge] warmup: " + v + " init failed:", e.message || e);
1228
+ });
1229
+ })(otherVendors[i]);
1230
+ }
1231
+
1232
+ // Detect installed vendors per-user (binary existence check)
1233
+ sm.installedVendors = detectInstalledVendors(linuxUser);
1234
+ sm.availableVendors = Object.keys(adapters);
1235
+
1236
+ // Send initial state to client
1237
+ send({
1238
+ type: "model_info",
1239
+ model: sm.currentModel || "",
1240
+ models: sm.availableModels || [],
1241
+ vendor: defaultVendor,
1242
+ availableVendors: sm.availableVendors,
1243
+ installedVendors: sm.installedVendors,
1244
+ });
1792
1245
  }
1793
1246
 
1794
1247
  async function setModel(session, model) {
1795
- if (session.worker) {
1796
- session.worker.send({ type: "set_model", model: model });
1797
- return;
1798
- }
1799
1248
  if (!session.queryInstance) {
1800
1249
  // No active query — just store the model for next startQuery
1801
1250
  sm.currentModel = model;
1251
+ // Don't send vendor here: session vendor not yet bound, let client keep its selection
1802
1252
  send({ type: "model_info", model: model, models: sm.availableModels || [] });
1803
1253
  send({ type: "config_state", model: sm.currentModel, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1804
1254
  return;
@@ -1806,7 +1256,8 @@ function createSDKBridge(opts) {
1806
1256
  try {
1807
1257
  await session.queryInstance.setModel(model);
1808
1258
  sm.currentModel = model;
1809
- send({ type: "model_info", model: model, models: sm.availableModels || [] });
1259
+ var sessionVendor = session.vendor || (adapter && adapter.vendor) || "claude";
1260
+ send({ type: "model_info", model: model, models: sm.availableModels || [], vendor: sessionVendor });
1810
1261
  send({ type: "config_state", model: sm.currentModel, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1811
1262
  } catch (e) {
1812
1263
  send({ type: "error", text: "Failed to switch model: " + (e.message || e) });
@@ -1814,26 +1265,20 @@ function createSDKBridge(opts) {
1814
1265
  }
1815
1266
 
1816
1267
  async function setEffort(session, effort) {
1817
- if (session.worker) {
1818
- session.worker.send({ type: "set_effort", effort: effort });
1819
- return;
1820
- }
1821
1268
  if (!session.queryInstance) {
1822
1269
  sm.currentEffort = effort;
1823
1270
  send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [] });
1824
1271
  return;
1825
1272
  }
1826
- // SDK Query interface has no setEffort method.
1827
- // Store the effort level — it will be applied via queryOptions.effort on the next query.
1273
+ // Route through QueryHandle (works for both in-process and worker paths)
1274
+ if (typeof session.queryInstance.setEffort === "function") {
1275
+ await session.queryInstance.setEffort(effort);
1276
+ }
1828
1277
  sm.currentEffort = effort;
1829
1278
  send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [] });
1830
1279
  }
1831
1280
 
1832
1281
  async function setPermissionMode(session, mode) {
1833
- if (session.worker) {
1834
- session.worker.send({ type: "set_permission_mode", mode: mode });
1835
- return;
1836
- }
1837
1282
  if (!session.queryInstance) {
1838
1283
  // No active query — just store the mode for next startQuery
1839
1284
  sm.currentPermissionMode = mode;
@@ -1841,6 +1286,7 @@ function createSDKBridge(opts) {
1841
1286
  return;
1842
1287
  }
1843
1288
  try {
1289
+ // Route through QueryHandle (works for both in-process and worker paths)
1844
1290
  await session.queryInstance.setPermissionMode(mode);
1845
1291
  sm.currentPermissionMode = mode;
1846
1292
  send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
@@ -1853,12 +1299,9 @@ function createSDKBridge(opts) {
1853
1299
  var session = sm.getActiveSession();
1854
1300
  if (!session) return;
1855
1301
  session.taskStopRequested = true;
1856
- if (session.worker) {
1857
- session.worker.send({ type: "stop_task", taskId: taskId });
1858
- return;
1859
- }
1860
1302
  if (!session.queryInstance) return;
1861
1303
  try {
1304
+ // Route through QueryHandle (works for both in-process and worker paths)
1862
1305
  await session.queryInstance.stopTask(taskId);
1863
1306
  } catch (e) {
1864
1307
  console.error("[sdk-bridge] stopTask error:", e.message);
@@ -1875,15 +1318,6 @@ function createSDKBridge(opts) {
1875
1318
  // within a conversation flow (session continuity).
1876
1319
  async function createMentionSession(opts) {
1877
1320
  // opts: { claudeMd, initialContext, initialMessage, onDelta, onDone, onError, onActivity }
1878
- var sdk;
1879
- try {
1880
- sdk = await getSDK();
1881
- } catch (e) {
1882
- opts.onError("Failed to load Claude SDK: " + (e.message || e));
1883
- return null;
1884
- }
1885
-
1886
- var mq = createMessageQueue();
1887
1321
  var abortController = new AbortController();
1888
1322
 
1889
1323
  // Current response callbacks (swapped on each pushMessage)
@@ -1896,114 +1330,86 @@ function createSDKBridge(opts) {
1896
1330
  var mentionBlocks = {};
1897
1331
  var alive = true;
1898
1332
 
1899
- var query;
1333
+ var handle;
1900
1334
  try {
1901
- var mentionQueryOptions = {
1902
- cwd: cwd,
1903
- systemPrompt: opts.claudeMd,
1904
- settingSources: ["user"],
1905
- includePartialMessages: true,
1906
- abortController: abortController,
1907
- canUseTool: opts.canUseTool || function (toolName, input) {
1908
- var whitelisted = checkToolWhitelist(toolName, input);
1909
- if (whitelisted) {
1910
- return Promise.resolve(whitelisted);
1911
- }
1912
- return Promise.resolve({
1913
- behavior: "deny",
1914
- message: "Read-only access. You cannot make changes via @mention.",
1915
- });
1335
+ handle = await adapter.createQuery({
1336
+ cwd: cwd,
1337
+ systemPrompt: opts.claudeMd,
1338
+ model: opts.model || undefined,
1339
+ toolServers: opts.includeMcpServers ? (mergeMcpServers(mcpServers, getRemoteMcpServers) || undefined) : undefined,
1340
+ abortController: abortController,
1341
+ canUseTool: opts.canUseTool || function (toolName, input) {
1342
+ var whitelisted = checkToolWhitelist(toolName, input);
1343
+ if (whitelisted) {
1344
+ return Promise.resolve(whitelisted);
1345
+ }
1346
+ return Promise.resolve({
1347
+ behavior: "deny",
1348
+ message: "Read-only access. You cannot make changes via @mention.",
1349
+ });
1350
+ },
1351
+ adapterOptions: {
1352
+ CLAUDE: {
1353
+ settingSources: ["user"],
1354
+ includePartialMessages: true,
1916
1355
  },
1917
- };
1918
- if (opts.model) mentionQueryOptions.model = opts.model;
1919
- if (opts.includeMcpServers) {
1920
- var _mentionMcp = mergeMcpServers(mcpServers, getRemoteMcpServers);
1921
- if (_mentionMcp) mentionQueryOptions.mcpServers = _mentionMcp;
1922
- }
1923
- query = sdk.query({
1924
- prompt: mq,
1925
- options: mentionQueryOptions,
1356
+ },
1926
1357
  });
1927
1358
  } catch (e) {
1928
1359
  opts.onError("Failed to create mention query: " + (e.message || e));
1929
1360
  return null;
1930
1361
  }
1362
+ var query = handle;
1931
1363
 
1932
1364
  // Push the initial message (context + question, with optional images)
1933
1365
  var initialPrompt = opts.initialContext + "\n\n" + opts.initialMessage;
1934
- var initialContent = [];
1935
- if (opts.initialImages && opts.initialImages.length > 0) {
1936
- for (var ii = 0; ii < opts.initialImages.length; ii++) {
1937
- initialContent.push({
1938
- type: "image",
1939
- source: { type: "base64", media_type: opts.initialImages[ii].mediaType, data: opts.initialImages[ii].data },
1940
- });
1941
- }
1942
- }
1943
- initialContent.push({ type: "text", text: initialPrompt });
1944
- mq.push({
1945
- type: "user",
1946
- message: { role: "user", content: initialContent },
1947
- });
1366
+ handle.pushMessage(initialPrompt, opts.initialImages || null);
1948
1367
 
1949
- // Background stream processing loop
1368
+ // Background stream processing loop (consumes flattened yokeType events)
1950
1369
  (async function () {
1951
1370
  try {
1952
- for await (var sdkMsg of query) {
1953
- if (sdkMsg.type === "stream_event" && sdkMsg.event) {
1954
- var evt = sdkMsg.event;
1955
-
1956
- // Track content blocks for activity reporting
1957
- if (evt.type === "content_block_start") {
1958
- var block = evt.content_block;
1959
- var idx = evt.index;
1960
- if (block.type === "thinking") {
1961
- mentionBlocks[idx] = { type: "thinking" };
1962
- if (currentOnActivity) currentOnActivity("thinking");
1963
- } else if (block.type === "tool_use") {
1964
- mentionBlocks[idx] = { type: "tool_use", name: block.name, inputJson: "" };
1965
- var toolLabel = block.name;
1966
- if (toolLabel === "Read") toolLabel = "Reading file...";
1967
- else if (toolLabel === "Grep") toolLabel = "Searching code...";
1968
- else if (toolLabel === "Glob") toolLabel = "Finding files...";
1969
- if (currentOnActivity) currentOnActivity(toolLabel);
1970
- } else if (block.type === "text") {
1971
- mentionBlocks[idx] = { type: "text" };
1972
- }
1973
- }
1974
-
1975
- if (evt.type === "content_block_delta" && evt.delta) {
1976
- if (evt.delta.type === "text_delta" && typeof evt.delta.text === "string") {
1977
- responseStreamedText = true;
1978
- responseFullText += evt.delta.text;
1979
- if (currentOnActivity) currentOnActivity(null); // clear activity on text
1980
- if (currentOnDelta) currentOnDelta(evt.delta.text);
1981
- } else if (evt.delta.type === "input_json_delta" && mentionBlocks[evt.index]) {
1982
- mentionBlocks[evt.index].inputJson += evt.delta.partial_json;
1983
- }
1984
- }
1985
-
1986
- if (evt.type === "content_block_stop") {
1987
- var blk = mentionBlocks[evt.index];
1988
- if (blk && blk.type === "tool_use") {
1989
- // Show what file is being read
1990
- var toolInput = {};
1991
- try { toolInput = JSON.parse(blk.inputJson); } catch (e) {}
1992
- if (blk.name === "Read" && toolInput.file_path) {
1993
- var fname = toolInput.file_path.split(/[/\\]/).pop();
1994
- if (currentOnActivity) currentOnActivity("Reading " + fname + "...");
1995
- } else if (blk.name === "Grep" && toolInput.pattern) {
1996
- if (currentOnActivity) currentOnActivity("Searching: " + toolInput.pattern.substring(0, 30) + "...");
1997
- } else if (blk.name === "Glob" && toolInput.pattern) {
1998
- if (currentOnActivity) currentOnActivity("Finding: " + toolInput.pattern.substring(0, 30) + "...");
1999
- }
1371
+ for await (var msg of query) {
1372
+ // Track content blocks for activity reporting
1373
+ if (msg.yokeType === "thinking_start") {
1374
+ mentionBlocks[msg.blockId] = { type: "thinking" };
1375
+ if (currentOnActivity) currentOnActivity("thinking");
1376
+ } else if (msg.yokeType === "tool_start") {
1377
+ mentionBlocks[msg.blockId] = { type: "tool_use", name: msg.toolName, inputJson: "" };
1378
+ var toolLabel = msg.toolName;
1379
+ if (toolLabel === "Read") toolLabel = "Reading file...";
1380
+ else if (toolLabel === "Grep") toolLabel = "Searching code...";
1381
+ else if (toolLabel === "Glob") toolLabel = "Finding files...";
1382
+ if (currentOnActivity) currentOnActivity(toolLabel);
1383
+ } else if (msg.yokeType === "text_start") {
1384
+ mentionBlocks[msg.blockId] = { type: "text" };
1385
+
1386
+ } else if (msg.yokeType === "text_delta" && typeof msg.text === "string") {
1387
+ responseStreamedText = true;
1388
+ responseFullText += msg.text;
1389
+ if (currentOnActivity) currentOnActivity(null);
1390
+ if (currentOnDelta) currentOnDelta(msg.text);
1391
+ } else if (msg.yokeType === "tool_input_delta" && mentionBlocks[msg.blockId]) {
1392
+ mentionBlocks[msg.blockId].inputJson += msg.partialJson;
1393
+
1394
+ } else if (msg.yokeType === "block_stop") {
1395
+ var blk = mentionBlocks[msg.blockId];
1396
+ if (blk && blk.type === "tool_use") {
1397
+ var toolInput = {};
1398
+ try { toolInput = JSON.parse(blk.inputJson); } catch (e) {}
1399
+ if (blk.name === "Read" && toolInput.file_path) {
1400
+ var fname = toolInput.file_path.split(/[/\\]/).pop();
1401
+ if (currentOnActivity) currentOnActivity("Reading " + fname + "...");
1402
+ } else if (blk.name === "Grep" && toolInput.pattern) {
1403
+ if (currentOnActivity) currentOnActivity("Searching: " + toolInput.pattern.substring(0, 30) + "...");
1404
+ } else if (blk.name === "Glob" && toolInput.pattern) {
1405
+ if (currentOnActivity) currentOnActivity("Finding: " + toolInput.pattern.substring(0, 30) + "...");
2000
1406
  }
2001
- delete mentionBlocks[evt.index];
2002
1407
  }
1408
+ delete mentionBlocks[msg.blockId];
2003
1409
 
2004
- } else if (sdkMsg.type === "assistant" && !responseStreamedText && sdkMsg.message && sdkMsg.message.content) {
1410
+ } else if (msg.yokeType === "message" && msg.messageRole === "assistant" && !responseStreamedText && msg.content) {
2005
1411
  // Fallback: if text was not streamed via deltas, extract from assistant message
2006
- var content = sdkMsg.message.content;
1412
+ var content = msg.content;
2007
1413
  if (Array.isArray(content)) {
2008
1414
  for (var ci = 0; ci < content.length; ci++) {
2009
1415
  if (content[ci].type === "text" && content[ci].text) {
@@ -2012,7 +1418,8 @@ function createSDKBridge(opts) {
2012
1418
  }
2013
1419
  }
2014
1420
  }
2015
- } else if (sdkMsg.type === "result") {
1421
+
1422
+ } else if (msg.yokeType === "result") {
2016
1423
  // One response complete. Signal done and reset for next message.
2017
1424
  if (currentOnActivity) currentOnActivity(null);
2018
1425
  var doneRef = currentOnDone;
@@ -2054,27 +1461,14 @@ function createSDKBridge(opts) {
2054
1461
  mentionBlocks = {};
2055
1462
  responseFullText = "";
2056
1463
  responseStreamedText = false;
2057
- var content = [];
2058
- if (images && images.length > 0) {
2059
- for (var pi = 0; pi < images.length; pi++) {
2060
- content.push({
2061
- type: "image",
2062
- source: { type: "base64", media_type: images[pi].mediaType, data: images[pi].data },
2063
- });
2064
- }
2065
- }
2066
- content.push({ type: "text", text: text });
2067
- mq.push({
2068
- type: "user",
2069
- message: { role: "user", content: content },
2070
- });
1464
+ handle.pushMessage(text, images || null);
2071
1465
  },
2072
1466
  abort: function () {
2073
1467
  try { abortController.abort(); } catch (e) {}
2074
1468
  },
2075
1469
  close: function () {
2076
1470
  alive = false;
2077
- try { mq.end(); } catch (e) {}
1471
+ try { handle.close(); } catch (e) {}
2078
1472
  },
2079
1473
  isAlive: function () { return alive; },
2080
1474
  };
@@ -2088,6 +1482,10 @@ function createSDKBridge(opts) {
2088
1482
  handleElicitation: handleElicitation,
2089
1483
  processQueryStream: processQueryStream,
2090
1484
  getOrCreateRewindQuery: getOrCreateRewindQuery,
1485
+ rewindPreview: rewindPreview,
1486
+ rewindExecuteFiles: rewindExecuteFiles,
1487
+ rollbackConversation: rollbackConversation,
1488
+ forkSession: forkSessionUnified,
2091
1489
  startQuery: startQuery,
2092
1490
  pushMessage: pushMessage,
2093
1491
  setModel: setModel,