clay-server 2.10.0 → 2.11.0-beta.10

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.
package/lib/sdk-bridge.js CHANGED
@@ -2,7 +2,10 @@ const crypto = require("crypto");
2
2
  var fs = require("fs");
3
3
  var path = require("path");
4
4
  var os = require("os");
5
- var { execSync } = require("child_process");
5
+ var net = require("net");
6
+ var { execSync, spawn } = require("child_process");
7
+ var { resolveOsUserInfo } = require("./os-users");
8
+ var usersModule = require("./users");
6
9
 
7
10
  // Async message queue for streaming input to SDK
8
11
  function createMessageQueue() {
@@ -55,6 +58,7 @@ function createSDKBridge(opts) {
55
58
  var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
56
59
  var onProcessingChanged = opts.onProcessingChanged || function () {};
57
60
 
61
+
58
62
  // --- Skill discovery helpers ---
59
63
 
60
64
  function discoverSkillDirs() {
@@ -243,6 +247,9 @@ function createSDKBridge(opts) {
243
247
  .map(function(c) { return c.text; })
244
248
  .join("");
245
249
  if (assistantText) {
250
+ if (session.responsePreview.length < 200) {
251
+ session.responsePreview += assistantText;
252
+ }
246
253
  sendAndRecord(session, { type: "delta", text: assistantText });
247
254
  }
248
255
  }
@@ -314,6 +321,7 @@ function createSDKBridge(opts) {
314
321
  session.blocks = {};
315
322
  session.sentToolResults = {};
316
323
  session.pendingPermissions = {};
324
+ session.pendingElicitations = {};
317
325
  // Record ask_user_answered for any leftover pending questions so replay pairs correctly
318
326
  var leftoverAskIds = Object.keys(session.pendingAskUser);
319
327
  for (var lai = 0; lai < leftoverAskIds.length; lai++) {
@@ -338,6 +346,27 @@ function createSDKBridge(opts) {
338
346
  if (parsed.fast_mode_state) {
339
347
  sendAndRecord(session, { type: "fast_mode_state", state: parsed.fast_mode_state });
340
348
  }
349
+ // Detect "Not logged in · Please run /login" from SDK.
350
+ // This is a short canned response with zero cost, not actual AI output.
351
+ var previewTrimmed = (session.responsePreview || "").trim();
352
+ var isZeroCost = !parsed.total_cost_usd || parsed.total_cost_usd === 0;
353
+ var isLoginPrompt = isZeroCost && previewTrimmed.length < 100
354
+ && /not logged in/i.test(previewTrimmed) && /\/login/i.test(previewTrimmed);
355
+ if (isLoginPrompt) {
356
+ var authUser = session.ownerId ? usersModule.findUserById(session.ownerId) : null;
357
+ var authLinuxUser = authUser && authUser.linuxUser ? authUser.linuxUser : null;
358
+ var canAutoLogin = !usersModule.isMultiUser()
359
+ || !!authLinuxUser
360
+ || (authUser && authUser.role === "admin");
361
+ sendAndRecord(session, {
362
+ type: "auth_required",
363
+ text: "Claude Code is not logged in.",
364
+ linuxUser: authLinuxUser,
365
+ canAutoLogin: canAutoLogin,
366
+ });
367
+ // Reset CLI session so next query starts fresh with new auth
368
+ session.cliSessionId = null;
369
+ }
341
370
  sendAndRecord(session, { type: "done", code: 0 });
342
371
  if (pushModule) {
343
372
  var preview = (session.responsePreview || "").replace(/\s+/g, " ").trim();
@@ -386,6 +415,7 @@ function createSDKBridge(opts) {
386
415
  usage: parsed.usage || null,
387
416
  lastToolName: parsed.last_tool_name || null,
388
417
  description: parsed.description || "",
418
+ summary: parsed.summary || null,
389
419
  });
390
420
  }
391
421
 
@@ -494,6 +524,524 @@ function createSDKBridge(opts) {
494
524
  // user messages with parent_tool_use_id contain tool_results — skip silently
495
525
  }
496
526
 
527
+ // --- MCP elicitation ---
528
+
529
+ function handleElicitation(session, request, opts) {
530
+ // Ralph Loop: auto-reject elicitation in autonomous mode
531
+ if (session.loop && session.loop.active && session.loop.role !== "crafting") {
532
+ return Promise.resolve({ action: "reject" });
533
+ }
534
+
535
+ return new Promise(function(resolve) {
536
+ var requestId = crypto.randomUUID();
537
+ if (!session.pendingElicitations) session.pendingElicitations = {};
538
+ session.pendingElicitations[requestId] = {
539
+ resolve: resolve,
540
+ request: request,
541
+ };
542
+ sendAndRecord(session, {
543
+ type: "elicitation_request",
544
+ requestId: requestId,
545
+ serverName: request.serverName,
546
+ message: request.message,
547
+ mode: request.mode || "form",
548
+ url: request.url || null,
549
+ elicitationId: request.elicitationId || null,
550
+ requestedSchema: request.requestedSchema || null,
551
+ });
552
+
553
+ if (pushModule) {
554
+ pushModule.sendPush({
555
+ type: "elicitation",
556
+ slug: slug,
557
+ title: (request.serverName || "MCP Server") + " needs input",
558
+ body: request.message || "Waiting for your response",
559
+ tag: "claude-elicitation",
560
+ });
561
+ }
562
+
563
+ if (opts.signal) {
564
+ opts.signal.addEventListener("abort", function() {
565
+ delete session.pendingElicitations[requestId];
566
+ resolve({ action: "reject" });
567
+ });
568
+ }
569
+ });
570
+ }
571
+
572
+ // --- Worker process management (OS-level multi-user) ---
573
+
574
+ var WORKER_SCRIPT = path.join(__dirname, "sdk-worker.js");
575
+
576
+ // resolveLinuxUser delegates to shared os-users utility
577
+ function resolveLinuxUser(username) {
578
+ return resolveOsUserInfo(username);
579
+ }
580
+
581
+ /**
582
+ * Spawn an SDK worker process running as the given Linux user.
583
+ * Returns a worker handle with send/kill/event methods.
584
+ */
585
+ function spawnWorker(linuxUser) {
586
+ var userInfo = resolveLinuxUser(linuxUser);
587
+ var socketId = crypto.randomUUID();
588
+ var socketPath = path.join(os.tmpdir(), "clay-worker-" + socketId + ".sock");
589
+
590
+ var worker = {
591
+ process: null,
592
+ connection: null,
593
+ socketPath: socketPath,
594
+ server: null,
595
+ messageHandlers: [],
596
+ ready: false,
597
+ readyPromise: null,
598
+ _readyResolve: null,
599
+ buffer: "",
600
+ };
601
+
602
+ worker.readyPromise = new Promise(function(resolve) {
603
+ worker._readyResolve = resolve;
604
+ });
605
+
606
+ // Create Unix socket server
607
+ worker.server = net.createServer(function(connection) {
608
+ worker.connection = connection;
609
+ connection.on("data", function(chunk) {
610
+ worker.buffer += chunk.toString();
611
+ var lines = worker.buffer.split("\n");
612
+ worker.buffer = lines.pop();
613
+ for (var i = 0; i < lines.length; i++) {
614
+ if (!lines[i].trim()) continue;
615
+ try {
616
+ var msg = JSON.parse(lines[i]);
617
+ if (msg.type === "ready") {
618
+ worker.ready = true;
619
+ if (worker._readyResolve) {
620
+ worker._readyResolve();
621
+ worker._readyResolve = null;
622
+ }
623
+ }
624
+ for (var h = 0; h < worker.messageHandlers.length; h++) {
625
+ worker.messageHandlers[h](msg);
626
+ }
627
+ } catch (e) {
628
+ console.error("[sdk-bridge] Failed to parse worker message:", e.message);
629
+ }
630
+ }
631
+ });
632
+ connection.on("error", function(err) {
633
+ console.error("[sdk-bridge] Worker connection error:", err.message);
634
+ });
635
+ });
636
+
637
+ worker.server.listen(socketPath, function() {
638
+ // Set socket permissions so the target user can connect
639
+ try { fs.chmodSync(socketPath, 0o777); } catch (e) {}
640
+
641
+ // Spawn worker process as the target Linux user
642
+ var workerEnv = {
643
+ HOME: userInfo.home,
644
+ USER: linuxUser,
645
+ PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin",
646
+ NODE_PATH: process.env.NODE_PATH || "",
647
+ LANG: process.env.LANG || "en_US.UTF-8",
648
+ };
649
+
650
+ worker.process = spawn(process.execPath, [WORKER_SCRIPT, socketPath], {
651
+ uid: userInfo.uid,
652
+ gid: userInfo.gid,
653
+ env: workerEnv,
654
+ cwd: cwd,
655
+ stdio: ["ignore", "pipe", "pipe"],
656
+ });
657
+
658
+ worker.process.stdout.on("data", function(data) {
659
+ console.log("[sdk-worker:" + linuxUser + "] " + data.toString().trim());
660
+ });
661
+ worker.process.stderr.on("data", function(data) {
662
+ console.error("[sdk-worker:" + linuxUser + "] " + data.toString().trim());
663
+ });
664
+
665
+ worker.process.on("exit", function(code, signal) {
666
+ console.log("[sdk-bridge] Worker for " + linuxUser + " exited (code=" + code + ", signal=" + signal + ")");
667
+ cleanupWorker(worker);
668
+ });
669
+ });
670
+
671
+ worker.send = function(msg) {
672
+ if (!worker.connection || worker.connection.destroyed) return;
673
+ try {
674
+ worker.connection.write(JSON.stringify(msg) + "\n");
675
+ } catch (e) {
676
+ console.error("[sdk-bridge] Failed to send to worker:", e.message);
677
+ }
678
+ };
679
+
680
+ worker.onMessage = function(handler) {
681
+ worker.messageHandlers.push(handler);
682
+ };
683
+
684
+ worker.kill = function() {
685
+ worker.send({ type: "shutdown" });
686
+ // Force kill after 3 seconds if still alive
687
+ setTimeout(function() {
688
+ if (worker.process && !worker.process.killed) {
689
+ try { worker.process.kill("SIGKILL"); } catch (e) {}
690
+ }
691
+ }, 3000);
692
+ cleanupWorker(worker);
693
+ };
694
+
695
+ return worker;
696
+ }
697
+
698
+ function cleanupWorker(worker) {
699
+ if (worker.connection && !worker.connection.destroyed) {
700
+ try { worker.connection.end(); } catch (e) {}
701
+ }
702
+ if (worker.server) {
703
+ try { worker.server.close(); } catch (e) {}
704
+ }
705
+ // Remove socket file
706
+ try { fs.unlinkSync(worker.socketPath); } catch (e) {}
707
+ worker.ready = false;
708
+ }
709
+
710
+ /**
711
+ * Start a query via a worker process running as the target Linux user.
712
+ * Mirrors the in-process startQuery flow but delegates SDK execution to the worker.
713
+ */
714
+ async function startQueryViaWorker(session, text, images, linuxUser) {
715
+ var worker;
716
+ try {
717
+ worker = spawnWorker(linuxUser);
718
+ session.worker = worker;
719
+ } catch (e) {
720
+ session.isProcessing = false;
721
+ onProcessingChanged();
722
+ sendAndRecord(session, { type: "error", text: "Failed to spawn worker for " + linuxUser + ": " + (e.message || e) });
723
+ sendAndRecord(session, { type: "done", code: 1 });
724
+ sm.broadcastSessionList();
725
+ return;
726
+ }
727
+
728
+ session.messageQueue = "worker"; // sentinel: messages go via worker IPC
729
+ session.blocks = {};
730
+ session.sentToolResults = {};
731
+ session.activeTaskToolIds = {};
732
+ session.pendingElicitations = {};
733
+ session.streamedText = false;
734
+ session.responsePreview = "";
735
+ session.abortController = { abort: function() { worker.send({ type: "abort" }); } };
736
+
737
+ // Build initial user message content
738
+ var content = [];
739
+ if (images && images.length > 0) {
740
+ for (var i = 0; i < images.length; i++) {
741
+ content.push({
742
+ type: "image",
743
+ source: { type: "base64", media_type: images[i].mediaType, data: images[i].data },
744
+ });
745
+ }
746
+ }
747
+ if (text) {
748
+ content.push({ type: "text", text: text });
749
+ }
750
+
751
+ var initialMessage = {
752
+ type: "user",
753
+ message: { role: "user", content: content },
754
+ };
755
+
756
+ // Build serializable query options (no callbacks, no AbortController)
757
+ var queryOptions = {
758
+ cwd: cwd,
759
+ settingSources: ["user", "project", "local"],
760
+ includePartialMessages: true,
761
+ enableFileCheckpointing: true,
762
+ extraArgs: { "replay-user-messages": null },
763
+ promptSuggestions: true,
764
+ agentProgressSummaries: true,
765
+ };
766
+
767
+ if (sm.currentModel) queryOptions.model = sm.currentModel;
768
+ if (sm.currentEffort) queryOptions.effort = sm.currentEffort;
769
+ if (sm.currentBetas && sm.currentBetas.length > 0) queryOptions.betas = sm.currentBetas;
770
+ if (sm.currentThinking === "disabled") {
771
+ queryOptions.thinking = { type: "disabled" };
772
+ } else if (sm.currentThinking === "budget" && sm.currentThinkingBudget) {
773
+ queryOptions.thinking = { type: "enabled", budgetTokens: sm.currentThinkingBudget };
774
+ }
775
+
776
+ if (dangerouslySkipPermissions) {
777
+ queryOptions.permissionMode = "bypassPermissions";
778
+ queryOptions.allowDangerouslySkipPermissions = true;
779
+ } else {
780
+ var modeToApply = session.acceptEditsAfterStart ? "acceptEdits" : sm.currentPermissionMode;
781
+ if (session.acceptEditsAfterStart) delete session.acceptEditsAfterStart;
782
+ if (modeToApply && modeToApply !== "default") {
783
+ queryOptions.permissionMode = modeToApply;
784
+ }
785
+ }
786
+
787
+ if (session.cliSessionId) {
788
+ queryOptions.resume = session.cliSessionId;
789
+ if (session.lastRewindUuid) {
790
+ queryOptions.resumeSessionAt = session.lastRewindUuid;
791
+ delete session.lastRewindUuid;
792
+ }
793
+ }
794
+
795
+ // Set up message handler for worker events
796
+ worker.onMessage(function(msg) {
797
+ switch (msg.type) {
798
+ case "sdk_event":
799
+ processSDKMessage(session, msg.event);
800
+ break;
801
+
802
+ case "permission_request":
803
+ handleCanUseTool(session, msg.toolName, msg.input, {
804
+ toolUseID: msg.toolUseId,
805
+ decisionReason: msg.decisionReason,
806
+ signal: session.abortController ? { addEventListener: function() {} } : undefined,
807
+ }).then(function(result) {
808
+ worker.send({ type: "permission_response", requestId: msg.requestId, result: result });
809
+ });
810
+ break;
811
+
812
+ case "ask_user_request":
813
+ // Delegate to the daemon's AskUserQuestion handling
814
+ handleCanUseTool(session, "AskUserQuestion", msg.input, {
815
+ toolUseID: msg.toolUseId,
816
+ signal: session.abortController ? { addEventListener: function() {} } : undefined,
817
+ }).then(function(result) {
818
+ worker.send({ type: "ask_user_response", toolUseId: msg.toolUseId, result: result });
819
+ });
820
+ break;
821
+
822
+ case "elicitation_request":
823
+ handleElicitation(session, {
824
+ serverName: msg.serverName,
825
+ message: msg.message,
826
+ mode: msg.mode,
827
+ url: msg.url,
828
+ elicitationId: msg.elicitationId,
829
+ requestedSchema: msg.requestedSchema,
830
+ }, {
831
+ signal: session.abortController ? { addEventListener: function() {} } : undefined,
832
+ }).then(function(result) {
833
+ worker.send({ type: "elicitation_response", requestId: msg.requestId, result: result });
834
+ });
835
+ break;
836
+
837
+ case "query_done":
838
+ // Stream ended normally
839
+ if (session.isProcessing && session.taskStopRequested) {
840
+ session.isProcessing = false;
841
+ onProcessingChanged();
842
+ sendAndRecord(session, { type: "info", text: "Interrupted \u00b7 What should Claude do instead?" });
843
+ sendAndRecord(session, { type: "done", code: 0 });
844
+ sm.broadcastSessionList();
845
+ }
846
+ cleanupSessionWorker(session);
847
+ if (session.onQueryComplete) {
848
+ try { session.onQueryComplete(session); } catch (err) {
849
+ console.error("[sdk-bridge] onQueryComplete error:", err.message || err);
850
+ }
851
+ }
852
+ break;
853
+
854
+ case "query_error":
855
+ if (session.isProcessing) {
856
+ session.isProcessing = false;
857
+ onProcessingChanged();
858
+ var isAbort = (msg.error && (msg.error.indexOf("AbortError") !== -1 || msg.error.indexOf("aborted") !== -1))
859
+ || session.taskStopRequested;
860
+ if (isAbort) {
861
+ if (!session.destroying) {
862
+ sendAndRecord(session, { type: "info", text: "Interrupted \u00b7 What should Claude do instead?" });
863
+ sendAndRecord(session, { type: "done", code: 0 });
864
+ }
865
+ } else if (session.destroying) {
866
+ console.log("[sdk-bridge] Suppressing worker error during shutdown for session " + session.localId);
867
+ } else {
868
+ var errDetail = msg.error || "Unknown error";
869
+ if (msg.stderr) errDetail += "\nstderr: " + msg.stderr;
870
+ if (msg.exitCode != null) errDetail += " (exitCode: " + msg.exitCode + ")";
871
+ console.error("[sdk-bridge] Worker query error for session " + session.localId + ":", errDetail);
872
+
873
+ var errLower = errDetail.toLowerCase();
874
+ var isContextOverflow = errLower.indexOf("prompt is too long") !== -1
875
+ || errLower.indexOf("context_length") !== -1
876
+ || errLower.indexOf("maximum context length") !== -1;
877
+ var isAuthError = errLower.indexOf("not logged in") !== -1
878
+ || errLower.indexOf("unauthenticated") !== -1
879
+ || errLower.indexOf("authentication") !== -1
880
+ || errLower.indexOf("sign in") !== -1
881
+ || errLower.indexOf("log in") !== -1
882
+ || errLower.indexOf("please login") !== -1;
883
+ if (isContextOverflow) {
884
+ sendAndRecord(session, { type: "context_overflow", text: "Conversation too long to continue." });
885
+ } else if (isAuthError) {
886
+ var authUser = session.ownerId ? usersModule.findUserById(session.ownerId) : null;
887
+ var authLinuxUser = authUser && authUser.linuxUser ? authUser.linuxUser : null;
888
+ // Determine if auto-login (auto terminal + claude) is safe:
889
+ // - Single-user mode: always ok
890
+ // - Multi-user + OS user isolation (linuxUser set): ok (isolated)
891
+ // - Multi-user + admin role: ok (they own the shared account)
892
+ // - Multi-user + regular user (no linuxUser): not ok (shared account)
893
+ var canAutoLogin = !usersModule.isMultiUser()
894
+ || !!authLinuxUser
895
+ || (authUser && authUser.role === "admin");
896
+ sendAndRecord(session, {
897
+ type: "auth_required",
898
+ text: "Claude Code is not logged in.",
899
+ linuxUser: authLinuxUser,
900
+ canAutoLogin: canAutoLogin,
901
+ });
902
+ } else {
903
+ sendAndRecord(session, { type: "error", text: "Claude process error: " + msg.error });
904
+ }
905
+ sendAndRecord(session, { type: "done", code: 1 });
906
+ if (pushModule) {
907
+ pushModule.sendPush({
908
+ type: "error",
909
+ slug: slug,
910
+ title: "Connection Lost",
911
+ body: "Claude process disconnected: " + (msg.error || "unknown error"),
912
+ tag: "claude-error",
913
+ });
914
+ }
915
+ }
916
+ sm.broadcastSessionList();
917
+ }
918
+ cleanupSessionWorker(session);
919
+ if (session.onQueryComplete) {
920
+ try { session.onQueryComplete(session); } catch (err) {
921
+ console.error("[sdk-bridge] onQueryComplete error:", err.message || err);
922
+ }
923
+ }
924
+ break;
925
+
926
+ case "model_changed":
927
+ sm.currentModel = msg.model;
928
+ send({ type: "model_info", model: msg.model, models: sm.availableModels || [] });
929
+ send({ type: "config_state", model: sm.currentModel, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
930
+ break;
931
+
932
+ case "effort_changed":
933
+ sm.currentEffort = msg.effort;
934
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [] });
935
+ break;
936
+
937
+ case "permission_mode_changed":
938
+ sm.currentPermissionMode = msg.mode;
939
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
940
+ break;
941
+
942
+ case "worker_error":
943
+ send({ type: "error", text: msg.error });
944
+ break;
945
+ }
946
+ });
947
+
948
+ // Wait for worker to be ready, then send query
949
+ try {
950
+ await worker.readyPromise;
951
+ } catch (e) {
952
+ session.isProcessing = false;
953
+ onProcessingChanged();
954
+ sendAndRecord(session, { type: "error", text: "Worker failed to connect: " + (e.message || e) });
955
+ sendAndRecord(session, { type: "done", code: 1 });
956
+ sm.broadcastSessionList();
957
+ cleanupSessionWorker(session);
958
+ return;
959
+ }
960
+
961
+ worker.send({
962
+ type: "query_start",
963
+ prompt: initialMessage,
964
+ options: queryOptions,
965
+ singleTurn: !!session.singleTurn,
966
+ });
967
+ }
968
+
969
+ function cleanupSessionWorker(session) {
970
+ session.queryInstance = null;
971
+ session.messageQueue = null;
972
+ session.abortController = null;
973
+ session.taskStopRequested = false;
974
+ session.pendingPermissions = {};
975
+ session.pendingAskUser = {};
976
+ session.pendingElicitations = {};
977
+ if (session.worker) {
978
+ session.worker.kill();
979
+ session.worker = null;
980
+ }
981
+ }
982
+
983
+ /**
984
+ * Run warmup via a worker process for a specific Linux user.
985
+ */
986
+ async function warmupViaWorker(linuxUser) {
987
+ var worker;
988
+ try {
989
+ worker = spawnWorker(linuxUser);
990
+ } catch (e) {
991
+ send({ type: "error", text: "Failed to spawn warmup worker for " + linuxUser + ": " + (e.message || e) });
992
+ return;
993
+ }
994
+
995
+ var warmupDone = false;
996
+
997
+ worker.onMessage(function(msg) {
998
+ if (msg.type === "warmup_done" && !warmupDone) {
999
+ warmupDone = true;
1000
+ var result = msg.result || {};
1001
+ var fsSkills = discoverSkillDirs();
1002
+ sm.skillNames = mergeSkills(result.skills, fsSkills);
1003
+ if (result.slashCommands) {
1004
+ var seen = new Set();
1005
+ var combined = [];
1006
+ var all = result.slashCommands.concat(Array.from(sm.skillNames));
1007
+ for (var k = 0; k < all.length; k++) {
1008
+ if (!seen.has(all[k])) {
1009
+ seen.add(all[k]);
1010
+ combined.push(all[k]);
1011
+ }
1012
+ }
1013
+ sm.slashCommands = combined;
1014
+ send({ type: "slash_commands", commands: sm.slashCommands });
1015
+ }
1016
+ if (result.model) {
1017
+ sm.currentModel = sm._savedDefaultModel || result.model;
1018
+ }
1019
+ sm.availableModels = result.models || [];
1020
+ send({ type: "model_info", model: sm.currentModel || "", models: sm.availableModels || [] });
1021
+ worker.kill();
1022
+ } else if (msg.type === "warmup_error" && !warmupDone) {
1023
+ warmupDone = true;
1024
+ send({ type: "error", text: result.error || "Warmup failed" });
1025
+ worker.kill();
1026
+ }
1027
+ });
1028
+
1029
+ try {
1030
+ await worker.readyPromise;
1031
+ } catch (e) {
1032
+ send({ type: "error", text: "Warmup worker failed to connect: " + (e.message || e) });
1033
+ cleanupWorker(worker);
1034
+ return;
1035
+ }
1036
+
1037
+ var warmupOptions = { cwd: cwd, settingSources: ["user", "project", "local"] };
1038
+ if (dangerouslySkipPermissions) {
1039
+ warmupOptions.permissionMode = "bypassPermissions";
1040
+ warmupOptions.allowDangerouslySkipPermissions = true;
1041
+ }
1042
+ worker.send({ type: "warmup", options: warmupOptions });
1043
+ }
1044
+
497
1045
  // --- SDK query lifecycle ---
498
1046
 
499
1047
  function handleCanUseTool(session, toolName, input, opts) {
@@ -681,11 +1229,29 @@ function createSDKBridge(opts) {
681
1229
  var isContextOverflow = errLower.indexOf("prompt is too long") !== -1
682
1230
  || errLower.indexOf("context_length") !== -1
683
1231
  || errLower.indexOf("maximum context length") !== -1;
1232
+ var isAuthError = errLower.indexOf("not logged in") !== -1
1233
+ || errLower.indexOf("unauthenticated") !== -1
1234
+ || errLower.indexOf("authentication") !== -1
1235
+ || errLower.indexOf("sign in") !== -1
1236
+ || errLower.indexOf("log in") !== -1
1237
+ || errLower.indexOf("please login") !== -1;
684
1238
  if (isContextOverflow) {
685
1239
  sendAndRecord(session, {
686
1240
  type: "context_overflow",
687
1241
  text: "Conversation too long to continue.",
688
1242
  });
1243
+ } else if (isAuthError) {
1244
+ var authUser = session.ownerId ? usersModule.findUserById(session.ownerId) : null;
1245
+ var authLinuxUser = authUser && authUser.linuxUser ? authUser.linuxUser : null;
1246
+ var canAutoLogin = !usersModule.isMultiUser()
1247
+ || !!authLinuxUser
1248
+ || (authUser && authUser.role === "admin");
1249
+ sendAndRecord(session, {
1250
+ type: "auth_required",
1251
+ text: "Claude Code is not logged in.",
1252
+ linuxUser: authLinuxUser,
1253
+ canAutoLogin: canAutoLogin,
1254
+ });
689
1255
  } else {
690
1256
  sendAndRecord(session, { type: "error", text: "Claude process error: " + err.message });
691
1257
  }
@@ -710,6 +1276,7 @@ function createSDKBridge(opts) {
710
1276
  session.taskStopRequested = false;
711
1277
  session.pendingPermissions = {};
712
1278
  session.pendingAskUser = {};
1279
+ session.pendingElicitations = {};
713
1280
  }
714
1281
  // Ralph Loop: notify completion so loop orchestrator can proceed
715
1282
  if (session.onQueryComplete) {
@@ -756,7 +1323,12 @@ function createSDKBridge(opts) {
756
1323
  };
757
1324
  }
758
1325
 
759
- async function startQuery(session, text, images) {
1326
+ async function startQuery(session, text, images, linuxUser) {
1327
+ // OS-level isolation: delegate to worker process if linuxUser is set
1328
+ if (linuxUser) {
1329
+ return startQueryViaWorker(session, text, images, linuxUser);
1330
+ }
1331
+
760
1332
  var sdk;
761
1333
  try {
762
1334
  sdk = await getSDK();
@@ -773,6 +1345,7 @@ function createSDKBridge(opts) {
773
1345
  session.blocks = {};
774
1346
  session.sentToolResults = {};
775
1347
  session.activeTaskToolIds = {};
1348
+ session.pendingElicitations = {};
776
1349
  session.streamedText = false;
777
1350
  session.responsePreview = "";
778
1351
 
@@ -805,9 +1378,13 @@ function createSDKBridge(opts) {
805
1378
  extraArgs: { "replay-user-messages": null },
806
1379
  abortController: session.abortController,
807
1380
  promptSuggestions: true,
1381
+ agentProgressSummaries: true,
808
1382
  canUseTool: function(toolName, input, toolOpts) {
809
1383
  return handleCanUseTool(session, toolName, input, toolOpts);
810
1384
  },
1385
+ onElicitation: function(request, elicitOpts) {
1386
+ return handleElicitation(session, request, elicitOpts);
1387
+ },
811
1388
  };
812
1389
 
813
1390
  if (sm.currentModel) {
@@ -822,6 +1399,12 @@ function createSDKBridge(opts) {
822
1399
  queryOptions.betas = sm.currentBetas;
823
1400
  }
824
1401
 
1402
+ if (sm.currentThinking === "disabled") {
1403
+ queryOptions.thinking = { type: "disabled" };
1404
+ } else if (sm.currentThinking === "budget" && sm.currentThinkingBudget) {
1405
+ queryOptions.thinking = { type: "enabled", budgetTokens: sm.currentThinkingBudget };
1406
+ }
1407
+
825
1408
  if (dangerouslySkipPermissions) {
826
1409
  queryOptions.permissionMode = "bypassPermissions";
827
1410
  queryOptions.allowDangerouslySkipPermissions = true;
@@ -887,10 +1470,16 @@ function createSDKBridge(opts) {
887
1470
  if (text) {
888
1471
  content.push({ type: "text", text: text });
889
1472
  }
890
- session.messageQueue.push({
1473
+ var userMsg = {
891
1474
  type: "user",
892
1475
  message: { role: "user", content: content },
893
- });
1476
+ };
1477
+ // Route through worker if active, otherwise direct to message queue
1478
+ if (session.worker) {
1479
+ session.worker.send({ type: "push_message", content: userMsg });
1480
+ } else {
1481
+ session.messageQueue.push(userMsg);
1482
+ }
894
1483
  }
895
1484
 
896
1485
  function permissionPushTitle(toolName, input) {
@@ -937,7 +1526,12 @@ function createSDKBridge(opts) {
937
1526
  }
938
1527
 
939
1528
  // SDK warmup: grab slash_commands, model, and available models from SDK init
940
- async function warmup() {
1529
+ async function warmup(linuxUser) {
1530
+ // OS-level isolation: delegate warmup to worker process
1531
+ if (linuxUser) {
1532
+ return warmupViaWorker(linuxUser);
1533
+ }
1534
+
941
1535
  try {
942
1536
  var sdk = await getSDK();
943
1537
  var ac = new AbortController();
@@ -992,6 +1586,10 @@ function createSDKBridge(opts) {
992
1586
  }
993
1587
 
994
1588
  async function setModel(session, model) {
1589
+ if (session.worker) {
1590
+ session.worker.send({ type: "set_model", model: model });
1591
+ return;
1592
+ }
995
1593
  if (!session.queryInstance) {
996
1594
  // No active query — just store the model for next startQuery
997
1595
  sm.currentModel = model;
@@ -1009,6 +1607,25 @@ function createSDKBridge(opts) {
1009
1607
  }
1010
1608
  }
1011
1609
 
1610
+ async function setEffort(session, effort) {
1611
+ if (session.worker) {
1612
+ session.worker.send({ type: "set_effort", effort: effort });
1613
+ return;
1614
+ }
1615
+ if (!session.queryInstance) {
1616
+ sm.currentEffort = effort;
1617
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [] });
1618
+ return;
1619
+ }
1620
+ try {
1621
+ await session.queryInstance.setEffort(effort);
1622
+ sm.currentEffort = effort;
1623
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [] });
1624
+ } catch (e) {
1625
+ send({ type: "error", text: "Failed to set effort: " + (e.message || e) });
1626
+ }
1627
+ }
1628
+
1012
1629
  async function setPermissionMode(session, mode) {
1013
1630
  // When dangerouslySkipPermissions is active, ignore mode changes from UI
1014
1631
  // to prevent accidentally downgrading from bypassPermissions
@@ -1016,6 +1633,10 @@ function createSDKBridge(opts) {
1016
1633
  send({ type: "config_state", model: sm.currentModel || "", mode: "bypassPermissions", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1017
1634
  return;
1018
1635
  }
1636
+ if (session.worker) {
1637
+ session.worker.send({ type: "set_permission_mode", mode: mode });
1638
+ return;
1639
+ }
1019
1640
  if (!session.queryInstance) {
1020
1641
  // No active query — just store the mode for next startQuery
1021
1642
  sm.currentPermissionMode = mode;
@@ -1033,8 +1654,13 @@ function createSDKBridge(opts) {
1033
1654
 
1034
1655
  async function stopTask(taskId) {
1035
1656
  var session = sm.getActiveSession();
1036
- if (!session || !session.queryInstance) return;
1657
+ if (!session) return;
1037
1658
  session.taskStopRequested = true;
1659
+ if (session.worker) {
1660
+ session.worker.send({ type: "stop_task", taskId: taskId });
1661
+ return;
1662
+ }
1663
+ if (!session.queryInstance) return;
1038
1664
  try {
1039
1665
  await session.queryInstance.stopTask(taskId);
1040
1666
  } catch (e) {
@@ -1051,11 +1677,13 @@ function createSDKBridge(opts) {
1051
1677
  createMessageQueue: createMessageQueue,
1052
1678
  processSDKMessage: processSDKMessage,
1053
1679
  handleCanUseTool: handleCanUseTool,
1680
+ handleElicitation: handleElicitation,
1054
1681
  processQueryStream: processQueryStream,
1055
1682
  getOrCreateRewindQuery: getOrCreateRewindQuery,
1056
1683
  startQuery: startQuery,
1057
1684
  pushMessage: pushMessage,
1058
1685
  setModel: setModel,
1686
+ setEffort: setEffort,
1059
1687
  setPermissionMode: setPermissionMode,
1060
1688
  isClaudeProcess: isClaudeProcess,
1061
1689
  permissionPushTitle: permissionPushTitle,