clay-server 2.31.0 → 2.32.0-beta.2
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/browser-mcp-server.js +32 -44
- package/lib/debate-mcp-server.js +14 -31
- package/lib/mcp-local.js +31 -1
- package/lib/project-connection.js +4 -2
- package/lib/project-filesystem.js +47 -1
- package/lib/project-http.js +75 -8
- package/lib/project-mcp.js +4 -0
- package/lib/project-sessions.js +88 -51
- package/lib/project-user-message.js +12 -7
- package/lib/project.js +204 -90
- package/lib/public/app.js +123 -448
- package/lib/public/codex-avatar.png +0 -0
- package/lib/public/css/debate.css +3 -2
- package/lib/public/css/filebrowser.css +91 -1
- package/lib/public/css/icon-strip.css +21 -5
- package/lib/public/css/input.css +181 -100
- package/lib/public/css/mates.css +43 -0
- package/lib/public/css/mention.css +48 -4
- package/lib/public/css/menus.css +1 -1
- package/lib/public/css/messages.css +2 -0
- package/lib/public/css/notifications-center.css +19 -0
- package/lib/public/index.html +46 -24
- package/lib/public/modules/app-connection.js +138 -37
- package/lib/public/modules/app-cursors.js +18 -17
- package/lib/public/modules/app-debate-ui.js +9 -9
- package/lib/public/modules/app-dm.js +170 -131
- package/lib/public/modules/app-favicon.js +28 -26
- package/lib/public/modules/app-header.js +79 -68
- package/lib/public/modules/app-home-hub.js +55 -47
- package/lib/public/modules/app-loop-ui.js +34 -18
- package/lib/public/modules/app-loop-wizard.js +6 -6
- package/lib/public/modules/app-messages.js +195 -152
- package/lib/public/modules/app-misc.js +23 -12
- package/lib/public/modules/app-notifications.js +97 -3
- package/lib/public/modules/app-panels.js +203 -49
- package/lib/public/modules/app-projects.js +159 -150
- package/lib/public/modules/app-rate-limit.js +5 -4
- package/lib/public/modules/app-rendering.js +149 -101
- package/lib/public/modules/app-skills-install.js +4 -4
- package/lib/public/modules/context-sources.js +12 -41
- package/lib/public/modules/dom-refs.js +21 -0
- package/lib/public/modules/filebrowser.js +173 -2
- package/lib/public/modules/input.js +86 -0
- package/lib/public/modules/mate-sidebar.js +38 -0
- package/lib/public/modules/mention.js +24 -6
- package/lib/public/modules/scheduler.js +1 -1
- package/lib/public/modules/sidebar-mates.js +66 -34
- package/lib/public/modules/sidebar-mobile.js +34 -30
- package/lib/public/modules/sidebar-projects.js +60 -57
- package/lib/public/modules/sidebar-sessions.js +75 -69
- package/lib/public/modules/sidebar.js +12 -20
- package/lib/public/modules/skills.js +8 -9
- package/lib/public/modules/sticky-notes.js +1 -2
- package/lib/public/modules/store.js +9 -2
- package/lib/public/modules/stt.js +4 -1
- package/lib/public/modules/tools.js +14 -9
- package/lib/sdk-bridge.js +511 -1113
- package/lib/sdk-message-processor.js +123 -134
- package/lib/sdk-worker.js +4 -0
- package/lib/server-dm.js +1 -0
- package/lib/server.js +86 -1
- package/lib/sessions.js +47 -36
- package/lib/ws-schema.js +2 -0
- package/lib/yoke/adapters/claude-worker.js +559 -0
- package/lib/yoke/adapters/claude.js +1418 -0
- package/lib/yoke/adapters/codex.js +968 -0
- package/lib/yoke/adapters/gemini.js +668 -0
- package/lib/yoke/codex-app-server.js +307 -0
- package/lib/yoke/index.js +199 -0
- package/lib/yoke/instructions.js +62 -0
- package/lib/yoke/interface.js +92 -0
- package/lib/yoke/mcp-bridge-server.js +294 -0
- package/lib/yoke/package.json +7 -0
- 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
|
|
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
|
|
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,
|
|
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
|
|
146
|
+
// End the query so the for-await loop in processQueryStream
|
|
138
147
|
// exits naturally, triggering the finally block cleanup.
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
|
|
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
|
-
|
|
236
|
-
var
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
var
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
322
|
-
if (
|
|
323
|
-
|
|
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
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
|
763
|
+
var handle;
|
|
1482
764
|
try {
|
|
1483
|
-
|
|
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
|
|
780
|
+
// Drain messages in background (stream stays alive until close)
|
|
1501
781
|
(async function() {
|
|
1502
|
-
try { for await (var msg of
|
|
782
|
+
try { for await (var msg of handle) {} } catch(e) {}
|
|
1503
783
|
})();
|
|
1504
784
|
|
|
1505
785
|
return {
|
|
1506
|
-
query:
|
|
786
|
+
query: handle,
|
|
1507
787
|
isTemp: true,
|
|
1508
|
-
cleanup: function() { try {
|
|
788
|
+
cleanup: function() { try { handle.close(); } catch(e) {} },
|
|
1509
789
|
};
|
|
1510
790
|
}
|
|
1511
791
|
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
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
|
-
//
|
|
1541
|
-
|
|
1542
|
-
if (
|
|
1543
|
-
|
|
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
|
-
|
|
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
|
-
|
|
941
|
+
claudeOpts.betas = sm.currentBetas;
|
|
1592
942
|
}
|
|
1593
|
-
|
|
1594
943
|
var thinkingMode = ls.thinking || sm.currentThinking;
|
|
1595
944
|
if (thinkingMode === "disabled") {
|
|
1596
|
-
|
|
945
|
+
claudeOpts.thinking = { type: "disabled" };
|
|
1597
946
|
} else if (thinkingMode === "budget") {
|
|
1598
947
|
var budgetTokens = ls.thinkingBudget || sm.currentThinkingBudget;
|
|
1599
|
-
if (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
|
-
|
|
957
|
+
claudeOpts.settings = Object.assign({}, claudeOpts.settings || {}, { disableAllHooks: ls.disableAllHooks });
|
|
1610
958
|
}
|
|
1611
959
|
|
|
1612
960
|
if (dangerouslySkipPermissions) {
|
|
1613
|
-
|
|
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
|
-
|
|
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
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
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
|
-
|
|
1634
|
-
|
|
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:", !!
|
|
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
|
-
|
|
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
|
|
1684
|
-
if (session.
|
|
1685
|
-
session.
|
|
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
|
-
//
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
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
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
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
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1827
|
-
|
|
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
|
|
1333
|
+
var handle;
|
|
1900
1334
|
try {
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
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
|
-
|
|
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
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
}
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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,
|