clay-server 2.31.0 → 2.32.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser-mcp-server.js +32 -44
- package/lib/codex-defaults.js +18 -0
- package/lib/debate-mcp-server.js +14 -31
- package/lib/mcp-local.js +31 -1
- package/lib/project-connection.js +9 -6
- package/lib/project-debate.js +8 -0
- package/lib/project-filesystem.js +47 -1
- package/lib/project-http.js +75 -8
- package/lib/project-mate-interaction.js +102 -16
- package/lib/project-mcp.js +4 -0
- package/lib/project-notifications.js +9 -0
- package/lib/project-sessions.js +94 -51
- package/lib/project-user-message.js +12 -7
- package/lib/project.js +234 -99
- package/lib/public/app.js +135 -454
- 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 +338 -104
- 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 +26 -0
- package/lib/public/css/tooltip.css +47 -0
- package/lib/public/index.html +78 -26
- 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 +175 -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 +199 -153
- package/lib/public/modules/app-misc.js +23 -12
- package/lib/public/modules/app-notifications.js +119 -9
- package/lib/public/modules/app-panels.js +203 -49
- package/lib/public/modules/app-projects.js +161 -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 +102 -66
- package/lib/public/modules/dom-refs.js +21 -0
- package/lib/public/modules/filebrowser.js +173 -2
- package/lib/public/modules/input.js +122 -0
- package/lib/public/modules/markdown.js +5 -1
- 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 +79 -35
- 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/terminal.js +12 -0
- package/lib/public/modules/tools.js +18 -13
- package/lib/public/modules/tooltip.js +32 -5
- package/lib/sdk-bridge.js +562 -1114
- package/lib/sdk-message-processor.js +150 -135
- package/lib/sdk-worker.js +4 -0
- package/lib/server-dm.js +1 -0
- package/lib/server.js +86 -1
- package/lib/sessions.js +81 -37
- package/lib/ws-schema.js +2 -0
- package/lib/yoke/adapters/claude-worker.js +559 -0
- package/lib/yoke/adapters/claude.js +1483 -0
- package/lib/yoke/adapters/codex.js +1121 -0
- package/lib/yoke/adapters/gemini.js +709 -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 +98 -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,11 +1,9 @@
|
|
|
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");
|
|
6
|
+
var { getCodexConfig } = require("./codex-defaults");
|
|
9
7
|
var { splitShellSegments, attachSkillDiscovery } = require("./sdk-skill-discovery");
|
|
10
8
|
var { createMessageQueue } = require("./sdk-message-queue");
|
|
11
9
|
var { attachMessageProcessor } = require("./sdk-message-processor");
|
|
@@ -74,17 +72,26 @@ function mergeMcpServers(localServers, getRemoteFn) {
|
|
|
74
72
|
merged[lk[i]] = localServers[lk[i]];
|
|
75
73
|
hasAny = true;
|
|
76
74
|
}
|
|
75
|
+
console.log("[mergeMcpServers] local servers:", lk.join(", ") || "(none)");
|
|
76
|
+
} else {
|
|
77
|
+
console.log("[mergeMcpServers] local servers: null");
|
|
77
78
|
}
|
|
78
79
|
if (typeof getRemoteFn === "function") {
|
|
79
80
|
var remote = getRemoteFn();
|
|
80
81
|
if (remote) {
|
|
81
82
|
var rk = Object.keys(remote);
|
|
83
|
+
console.log("[mergeMcpServers] remote servers:", rk.join(", ") || "(none)");
|
|
82
84
|
for (var j = 0; j < rk.length; j++) {
|
|
83
85
|
merged[rk[j]] = remote[rk[j]];
|
|
84
86
|
hasAny = true;
|
|
85
87
|
}
|
|
88
|
+
} else {
|
|
89
|
+
console.log("[mergeMcpServers] remote servers: null/empty");
|
|
86
90
|
}
|
|
91
|
+
} else {
|
|
92
|
+
console.log("[mergeMcpServers] getRemoteFn not a function");
|
|
87
93
|
}
|
|
94
|
+
console.log("[mergeMcpServers] merged result:", Object.keys(merged).join(", ") || "(none)");
|
|
88
95
|
return hasAny ? merged : null;
|
|
89
96
|
}
|
|
90
97
|
|
|
@@ -95,12 +102,16 @@ function createSDKBridge(opts) {
|
|
|
95
102
|
var send = opts.send; // broadcast to all clients
|
|
96
103
|
var pushModule = opts.pushModule;
|
|
97
104
|
var getNotificationsModule = opts.getNotificationsModule || function () { return null; };
|
|
98
|
-
var
|
|
105
|
+
var adapter = opts.adapter;
|
|
106
|
+
var adapters = opts.adapters || {};
|
|
99
107
|
var mateDisplayName = opts.mateDisplayName || "";
|
|
100
108
|
var isMate = opts.isMate || (slug.indexOf("mate-") === 0);
|
|
101
109
|
var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
|
|
102
110
|
var mcpServers = opts.mcpServers || null;
|
|
103
111
|
var getRemoteMcpServers = opts.getRemoteMcpServers || null;
|
|
112
|
+
var clayPort = opts.clayPort || 2633;
|
|
113
|
+
var clayTls = opts.clayTls || false;
|
|
114
|
+
var clayAuthToken = opts.clayAuthToken || null;
|
|
104
115
|
var onProcessingChanged = opts.onProcessingChanged || function () {};
|
|
105
116
|
var onTurnDone = opts.onTurnDone || null;
|
|
106
117
|
|
|
@@ -121,11 +132,10 @@ function createSDKBridge(opts) {
|
|
|
121
132
|
_idleReaperTimer = setInterval(function () {
|
|
122
133
|
var now = Date.now();
|
|
123
134
|
sm.sessions.forEach(function (session) {
|
|
124
|
-
// Skip sessions that are actively processing, have no query,
|
|
135
|
+
// Skip sessions that are actively processing, have no query,
|
|
125
136
|
// or are single-turn (Ralph Loop — managed by onQueryComplete).
|
|
126
137
|
if (session.isProcessing) return;
|
|
127
138
|
if (!session.queryInstance) return;
|
|
128
|
-
if (session.worker) return;
|
|
129
139
|
if (session.singleTurn) return;
|
|
130
140
|
if (session.destroying) return;
|
|
131
141
|
|
|
@@ -134,9 +144,12 @@ function createSDKBridge(opts) {
|
|
|
134
144
|
console.log("[sdk-bridge] Reaping idle session " + session.localId +
|
|
135
145
|
" (idle " + Math.round((now - lastActivity) / 60000) + "min)" +
|
|
136
146
|
(session.title ? " title=" + JSON.stringify(session.title) : ""));
|
|
137
|
-
// End the
|
|
147
|
+
// End the query so the for-await loop in processQueryStream
|
|
138
148
|
// exits naturally, triggering the finally block cleanup.
|
|
139
|
-
|
|
149
|
+
// Works for both in-process (messageQueue.end) and worker (handle.close) paths.
|
|
150
|
+
if (session.queryInstance && typeof session.queryInstance.close === "function") {
|
|
151
|
+
try { session.queryInstance.close(); } catch (e) {}
|
|
152
|
+
} else if (session.messageQueue && typeof session.messageQueue.end === "function") {
|
|
140
153
|
try { session.messageQueue.end(); } catch (e) {}
|
|
141
154
|
}
|
|
142
155
|
}
|
|
@@ -159,17 +172,58 @@ function createSDKBridge(opts) {
|
|
|
159
172
|
var mergeSkills = skills.mergeSkills;
|
|
160
173
|
|
|
161
174
|
// --- Message processing (extracted to sdk-message-processor.js) ---
|
|
175
|
+
// Auto-generate a session title via YOKE adapter.generateTitle().
|
|
176
|
+
// Triggered by sdk-message-processor after AUTO_TITLE_TURN_THRESHOLD turns.
|
|
177
|
+
function autoGenerateTitle(session) {
|
|
178
|
+
if (typeof adapter.generateTitle !== "function") {
|
|
179
|
+
console.log("[auto-title] adapter.generateTitle not available for vendor=" + adapter.vendor);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
var userMessages = [];
|
|
183
|
+
for (var i = 0; i < session.history.length; i++) {
|
|
184
|
+
var entry = session.history[i];
|
|
185
|
+
if (entry.type === "user_message" && entry.text) {
|
|
186
|
+
userMessages.push(entry.text.substring(0, 200));
|
|
187
|
+
if (userMessages.length >= 5) break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (userMessages.length === 0) {
|
|
191
|
+
console.log("[auto-title] No user messages found in session " + session.localId);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
console.log("[auto-title] Calling adapter.generateTitle with " + userMessages.length + " messages for session " + session.localId);
|
|
195
|
+
|
|
196
|
+
adapter.generateTitle(userMessages, { cwd: cwd }).then(function(title) {
|
|
197
|
+
if (!title || title.length < 2) return;
|
|
198
|
+
title = title.substring(0, 100);
|
|
199
|
+
if (!session.titleManuallySet) {
|
|
200
|
+
session.title = title;
|
|
201
|
+
session.titleAutoGenerated = true;
|
|
202
|
+
sm.saveSessionFile(session);
|
|
203
|
+
sm.broadcastSessionList();
|
|
204
|
+
if (session.cliSessionId && typeof adapter.renameSession === "function") {
|
|
205
|
+
adapter.renameSession(session.cliSessionId, title, { dir: cwd }).catch(function () {});
|
|
206
|
+
}
|
|
207
|
+
console.log("[auto-title] Generated title for session " + session.localId + ": " + title);
|
|
208
|
+
}
|
|
209
|
+
}).catch(function(e) {
|
|
210
|
+
console.error("[auto-title] Failed:", e.message || e);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
162
214
|
var msgProcessor = attachMessageProcessor({
|
|
163
215
|
sm: sm,
|
|
164
216
|
send: send,
|
|
165
217
|
slug: slug,
|
|
218
|
+
cwd: cwd,
|
|
166
219
|
isMate: isMate,
|
|
167
220
|
mateDisplayName: mateDisplayName,
|
|
168
221
|
pushModule: pushModule,
|
|
169
222
|
getNotificationsModule: getNotificationsModule,
|
|
170
|
-
|
|
223
|
+
adapter: adapter,
|
|
171
224
|
onProcessingChanged: onProcessingChanged,
|
|
172
225
|
onTurnDone: onTurnDone,
|
|
226
|
+
onAutoTitle: function (session) { autoGenerateTitle(session); },
|
|
173
227
|
opts: opts,
|
|
174
228
|
discoverSkillDirs: discoverSkillDirs,
|
|
175
229
|
mergeSkills: mergeSkills,
|
|
@@ -223,817 +277,48 @@ function createSDKBridge(opts) {
|
|
|
223
277
|
});
|
|
224
278
|
}
|
|
225
279
|
|
|
226
|
-
// --- Worker process management (OS-level multi-user) ---
|
|
227
|
-
|
|
228
|
-
var WORKER_SCRIPT = path.join(__dirname, "sdk-worker.js");
|
|
229
280
|
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
|
|
281
|
+
// --- Linux user project directory setup ---
|
|
282
|
+
// Ensures the linux user's .claude project directory exists and is writable,
|
|
283
|
+
// then pre-copies CLI session file if needed. Called before starting a query
|
|
284
|
+
// so the worker can resume from the correct session file.
|
|
285
|
+
function ensureLinuxUserProjectDir(linuxUser, session) {
|
|
234
286
|
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;
|
|
287
|
+
var configMod = require("./config");
|
|
288
|
+
var osUsersMod = require("./os-users");
|
|
289
|
+
var originalHome = configMod.REAL_HOME || require("os").homedir();
|
|
290
|
+
var linuxUserHome = osUsersMod.getLinuxUserHome(linuxUser);
|
|
291
|
+
var uid = osUsersMod.getLinuxUserUid(linuxUser);
|
|
292
|
+
if (originalHome !== linuxUserHome && uid != null) {
|
|
293
|
+
var projectSlug = (cwd || "").replace(/\//g, "-");
|
|
294
|
+
var dstDir = path.join(linuxUserHome, ".claude", "projects", projectSlug);
|
|
295
|
+
// Create and chown the project directory once
|
|
296
|
+
if (!fs.existsSync(dstDir)) {
|
|
297
|
+
fs.mkdirSync(dstDir, { recursive: true });
|
|
298
|
+
try { require("child_process").execSync("chown -R " + uid + " " + JSON.stringify(path.join(linuxUserHome, ".claude"))); } catch (e2) {}
|
|
299
|
+
} else {
|
|
320
300
|
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);
|
|
301
|
+
var dirStat = fs.statSync(dstDir);
|
|
302
|
+
if (dirStat.uid !== uid) {
|
|
303
|
+
require("child_process").execSync("chown " + uid + " " + JSON.stringify(dstDir));
|
|
574
304
|
}
|
|
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) {}
|
|
305
|
+
} catch (e2) {}
|
|
601
306
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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);
|
|
878
|
-
}
|
|
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;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
case "model_changed":
|
|
889
|
-
sm.currentModel = msg.model;
|
|
890
|
-
send({ type: "model_info", model: msg.model, models: sm.availableModels || [] });
|
|
891
|
-
send({ type: "config_state", model: sm.currentModel, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
892
|
-
break;
|
|
893
|
-
|
|
894
|
-
case "effort_changed":
|
|
895
|
-
sm.currentEffort = msg.effort;
|
|
896
|
-
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [] });
|
|
897
|
-
break;
|
|
898
|
-
|
|
899
|
-
case "permission_mode_changed":
|
|
900
|
-
sm.currentPermissionMode = msg.mode;
|
|
901
|
-
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
902
|
-
break;
|
|
903
|
-
|
|
904
|
-
case "worker_error":
|
|
905
|
-
send({ type: "error", text: msg.error });
|
|
906
|
-
break;
|
|
907
|
-
}
|
|
908
|
-
});
|
|
909
|
-
|
|
910
|
-
// Wait for worker to be ready, then send query
|
|
911
|
-
if (!reusingWorker) {
|
|
912
|
-
perf("awaiting readyPromise");
|
|
913
|
-
try {
|
|
914
|
-
await worker.readyPromise;
|
|
915
|
-
perf("readyPromise resolved");
|
|
916
|
-
} catch (e) {
|
|
917
|
-
session.isProcessing = false;
|
|
918
|
-
onProcessingChanged();
|
|
919
|
-
sendAndRecord(session, { type: "error", text: "Worker failed to connect: " + (e.message || e) });
|
|
920
|
-
sendAndRecord(session, { type: "done", code: 1 });
|
|
921
|
-
sm.broadcastSessionList();
|
|
922
|
-
killSessionWorker(session);
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
perf("sending query_start to worker");
|
|
928
|
-
worker.send({
|
|
929
|
-
type: "query_start",
|
|
930
|
-
prompt: initialMessage,
|
|
931
|
-
options: queryOptions,
|
|
932
|
-
mcpDescriptors: _mcpDescriptors,
|
|
933
|
-
singleTurn: !!session.singleTurn,
|
|
934
|
-
originalHome: require("./config").REAL_HOME || null,
|
|
935
|
-
projectPath: session.cwd || null,
|
|
936
|
-
_perfT0: t0,
|
|
937
|
-
});
|
|
938
|
-
perf("query_start sent");
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
function cleanupSessionWorker(session, fromWorker) {
|
|
942
|
-
console.log("[sdk-bridge] cleanupSessionWorker() called, localId=" + session.localId +
|
|
943
|
-
" fromWorkerPid=" + (fromWorker && fromWorker.process ? fromWorker.process.pid : "none") +
|
|
944
|
-
" currentWorkerPid=" + (session.worker && session.worker.process ? session.worker.process.pid : "none") +
|
|
945
|
-
" stack=" + new Error().stack.split("\n").slice(1, 4).join(" | "));
|
|
946
|
-
// If called from a specific worker's exit/error handler, only cleanup if
|
|
947
|
-
// that worker is still the session's current worker. Prevents stale
|
|
948
|
-
// worker exit events from killing a newer worker.
|
|
949
|
-
if (fromWorker && session.worker && session.worker !== fromWorker) {
|
|
950
|
-
console.log("[sdk-bridge] cleanupSessionWorker: stale worker guard triggered, skipping");
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
session.queryInstance = null;
|
|
954
|
-
session.messageQueue = null;
|
|
955
|
-
session.abortController = null;
|
|
956
|
-
session.taskStopRequested = false;
|
|
957
|
-
session.pendingPermissions = {};
|
|
958
|
-
session.pendingAskUser = {};
|
|
959
|
-
session.pendingElicitations = {};
|
|
960
|
-
// Keep the worker alive between queries so the SDK can maintain session
|
|
961
|
-
// state in memory. Killing the worker after each query forces resume from
|
|
962
|
-
// disk, but the SDK may not save the session file on abort, causing
|
|
963
|
-
// "no conversation found" and losing all conversation history.
|
|
964
|
-
// The worker is only killed when the UI session is destroyed or on error.
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
// Force-kill the worker and remove it from the session.
|
|
968
|
-
// Used when the session is destroyed or on unrecoverable errors.
|
|
969
|
-
function killSessionWorker(session) {
|
|
970
|
-
if (session.worker) {
|
|
971
|
-
session._workerExitPromise = session.worker.exitPromise;
|
|
972
|
-
session.worker.kill();
|
|
973
|
-
session.worker = null;
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
/**
|
|
978
|
-
* Run warmup via a worker process for a specific Linux user.
|
|
979
|
-
*/
|
|
980
|
-
async function warmupViaWorker(linuxUser) {
|
|
981
|
-
var worker;
|
|
982
|
-
try {
|
|
983
|
-
worker = spawnWorker(linuxUser);
|
|
984
|
-
} catch (e) {
|
|
985
|
-
send({ type: "error", text: "Failed to spawn warmup worker for " + linuxUser + ": " + (e.message || e) });
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
var warmupDone = false;
|
|
990
|
-
|
|
991
|
-
worker.onMessage(function(msg) {
|
|
992
|
-
if (msg.type === "warmup_done" && !warmupDone) {
|
|
993
|
-
warmupDone = true;
|
|
994
|
-
var result = msg.result || {};
|
|
995
|
-
var fsSkills = discoverSkillDirs();
|
|
996
|
-
sm.skillNames = mergeSkills(result.skills, fsSkills);
|
|
997
|
-
if (result.slashCommands) {
|
|
998
|
-
var seen = new Set();
|
|
999
|
-
var combined = [];
|
|
1000
|
-
var all = result.slashCommands.concat(Array.from(sm.skillNames));
|
|
1001
|
-
for (var k = 0; k < all.length; k++) {
|
|
1002
|
-
if (!seen.has(all[k])) {
|
|
1003
|
-
seen.add(all[k]);
|
|
1004
|
-
combined.push(all[k]);
|
|
1005
|
-
}
|
|
307
|
+
// Pre-copy CLI session file so the worker can resume the conversation
|
|
308
|
+
if (session.cliSessionId) {
|
|
309
|
+
var sessionFileName = session.cliSessionId + ".jsonl";
|
|
310
|
+
var srcFile = path.join(originalHome, ".claude", "projects", projectSlug, sessionFileName);
|
|
311
|
+
var dstFile = path.join(dstDir, sessionFileName);
|
|
312
|
+
if (fs.existsSync(srcFile) && !fs.existsSync(dstFile)) {
|
|
313
|
+
fs.copyFileSync(srcFile, dstFile);
|
|
314
|
+
try { require("child_process").execSync("chown " + uid + " " + JSON.stringify(dstFile)); } catch (e2) {}
|
|
315
|
+
console.log("[sdk-bridge] Pre-copied CLI session " + session.cliSessionId + " to " + linuxUser);
|
|
1006
316
|
}
|
|
1007
|
-
sm.slashCommands = combined;
|
|
1008
|
-
send({ type: "slash_commands", commands: sm.slashCommands });
|
|
1009
317
|
}
|
|
1010
|
-
if (result.model) {
|
|
1011
|
-
sm.currentModel = sm._savedDefaultModel || result.model;
|
|
1012
|
-
}
|
|
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
318
|
}
|
|
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;
|
|
319
|
+
} catch (copyErr) {
|
|
320
|
+
console.log("[sdk-bridge] Dir setup / session pre-copy skipped:", copyErr.message);
|
|
1029
321
|
}
|
|
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
322
|
}
|
|
1038
323
|
|
|
1039
324
|
// --- SDK query lifecycle ---
|
|
@@ -1235,6 +520,7 @@ function createSDKBridge(opts) {
|
|
|
1235
520
|
toolInput: input,
|
|
1236
521
|
toolUseId: opts.toolUseID,
|
|
1237
522
|
decisionReason: opts.decisionReason || "",
|
|
523
|
+
vendor: session.vendor || (adapter && adapter.vendor) || "claude",
|
|
1238
524
|
};
|
|
1239
525
|
sendAndRecord(session, permMsg);
|
|
1240
526
|
onProcessingChanged(); // update cross-project permission badge
|
|
@@ -1338,19 +624,55 @@ function createSDKBridge(opts) {
|
|
|
1338
624
|
// Capture references at start so we only clean up OUR resources in finally,
|
|
1339
625
|
// not resources from a newer query that may have been created after an abort.
|
|
1340
626
|
var myQueryInstance = session.queryInstance;
|
|
1341
|
-
var myMessageQueue = session.messageQueue;
|
|
1342
627
|
var myAbortController = session.abortController;
|
|
628
|
+
console.log("[sdk-bridge] processQueryStream: starting for-await loop, vendor=" + (session.vendor || adapter.vendor));
|
|
1343
629
|
try {
|
|
1344
630
|
for await (var msg of myQueryInstance) {
|
|
631
|
+
if (msg && msg.yokeType !== "text_delta" && msg.yokeType !== "thinking_delta" && msg.yokeType !== "tool_input_delta") {
|
|
632
|
+
console.log("[sdk-bridge] processQueryStream: received event yokeType=" + msg.yokeType);
|
|
633
|
+
}
|
|
634
|
+
// Handle worker meta events (context_usage, model_changed, etc.)
|
|
635
|
+
if (msg && msg.type === "_worker_meta") {
|
|
636
|
+
var metaData = msg.data || {};
|
|
637
|
+
switch (msg.subtype) {
|
|
638
|
+
case "context_usage":
|
|
639
|
+
session.lastContextUsage = metaData.data;
|
|
640
|
+
sendToSession(session, { type: "context_usage", data: metaData.data });
|
|
641
|
+
break;
|
|
642
|
+
case "model_changed":
|
|
643
|
+
sm.currentModel = metaData.model;
|
|
644
|
+
send({ type: "model_info", model: metaData.model, models: sm.availableModels || [], vendor: adapter.vendor });
|
|
645
|
+
send({ type: "config_state", model: sm.currentModel, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
646
|
+
break;
|
|
647
|
+
case "effort_changed":
|
|
648
|
+
sm.currentEffort = metaData.effort;
|
|
649
|
+
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [] });
|
|
650
|
+
break;
|
|
651
|
+
case "permission_mode_changed":
|
|
652
|
+
sm.currentPermissionMode = metaData.mode;
|
|
653
|
+
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
654
|
+
break;
|
|
655
|
+
case "worker_error":
|
|
656
|
+
send({ type: "error", text: metaData.error });
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
1345
661
|
processSDKMessage(session, msg);
|
|
1346
662
|
}
|
|
1347
663
|
// (getContextUsage moved to processSDKMessage result handler -- fire-and-forget)
|
|
1348
664
|
// Stream ended normally after a task stop — no "result" message was sent,
|
|
1349
665
|
// so the session is still marked as processing. Send interrupted feedback.
|
|
666
|
+
console.log("[sdk-bridge] processQueryStream ended: isProcessing=" + session.isProcessing + " taskStopRequested=" + session.taskStopRequested);
|
|
1350
667
|
if (session.isProcessing && session.taskStopRequested) {
|
|
1351
668
|
session.isProcessing = false;
|
|
1352
669
|
onProcessingChanged();
|
|
1353
|
-
|
|
670
|
+
send({ type: "status", processing: false });
|
|
671
|
+
sendAndRecord(session, { type: "thinking_stop" });
|
|
672
|
+
var interruptMsg = (session.vendor === "codex")
|
|
673
|
+
? "\u25a0 Conversation interrupted - tell the model what to do differently."
|
|
674
|
+
: "Interrupted \u00b7 What should Claude do instead?";
|
|
675
|
+
sendAndRecord(session, { type: "info", text: interruptMsg });
|
|
1354
676
|
sendAndRecord(session, { type: "done", code: 0 });
|
|
1355
677
|
sm.broadcastSessionList();
|
|
1356
678
|
}
|
|
@@ -1360,7 +682,11 @@ function createSDKBridge(opts) {
|
|
|
1360
682
|
onProcessingChanged();
|
|
1361
683
|
if (err.name === "AbortError" || (myAbortController && myAbortController.signal.aborted) || session.taskStopRequested) {
|
|
1362
684
|
if (!session.destroying) {
|
|
1363
|
-
sendAndRecord(session, { type: "
|
|
685
|
+
sendAndRecord(session, { type: "thinking_stop" });
|
|
686
|
+
var interruptMsg2 = (session.vendor === "codex")
|
|
687
|
+
? "\u25a0 Conversation interrupted - tell the model what to do differently."
|
|
688
|
+
: "Interrupted \u00b7 What should Claude do instead?";
|
|
689
|
+
sendAndRecord(session, { type: "info", text: interruptMsg2 });
|
|
1364
690
|
sendAndRecord(session, { type: "done", code: 0 });
|
|
1365
691
|
}
|
|
1366
692
|
} else if (session.destroying) {
|
|
@@ -1433,7 +759,7 @@ function createSDKBridge(opts) {
|
|
|
1433
759
|
} catch (e) {}
|
|
1434
760
|
session.queryInstance = null;
|
|
1435
761
|
}
|
|
1436
|
-
|
|
762
|
+
session.messageQueue = null;
|
|
1437
763
|
if (session.abortController === myAbortController) session.abortController = null;
|
|
1438
764
|
session.taskStopRequested = false;
|
|
1439
765
|
session.pendingPermissions = {};
|
|
@@ -1478,58 +804,157 @@ function createSDKBridge(opts) {
|
|
|
1478
804
|
async function getOrCreateRewindQuery(session) {
|
|
1479
805
|
if (session.queryInstance) return { query: session.queryInstance, isTemp: false, cleanup: function() {} };
|
|
1480
806
|
|
|
1481
|
-
var
|
|
807
|
+
var handle;
|
|
1482
808
|
try {
|
|
1483
|
-
|
|
809
|
+
handle = await adapter.createQuery({
|
|
810
|
+
cwd: cwd,
|
|
811
|
+
resumeSessionId: session.cliSessionId,
|
|
812
|
+
adapterOptions: {
|
|
813
|
+
CLAUDE: {
|
|
814
|
+
settingSources: ["user", "project", "local"],
|
|
815
|
+
enableFileCheckpointing: true,
|
|
816
|
+
},
|
|
817
|
+
},
|
|
818
|
+
});
|
|
1484
819
|
} catch (e) {
|
|
1485
820
|
sendAndRecord(session, { type: "error", text: "Failed to load Claude SDK: " + (e.message || e) });
|
|
1486
821
|
throw e;
|
|
1487
822
|
}
|
|
1488
|
-
var mq = createMessageQueue();
|
|
1489
823
|
|
|
1490
|
-
|
|
1491
|
-
prompt: mq,
|
|
1492
|
-
options: {
|
|
1493
|
-
cwd: cwd,
|
|
1494
|
-
settingSources: ["user", "project", "local"],
|
|
1495
|
-
enableFileCheckpointing: true,
|
|
1496
|
-
resume: session.cliSessionId,
|
|
1497
|
-
},
|
|
1498
|
-
});
|
|
1499
|
-
|
|
1500
|
-
// Drain messages in background (stream stays alive until mq.end())
|
|
824
|
+
// Drain messages in background (stream stays alive until close)
|
|
1501
825
|
(async function() {
|
|
1502
|
-
try { for await (var msg of
|
|
826
|
+
try { for await (var msg of handle) {} } catch(e) {}
|
|
1503
827
|
})();
|
|
1504
828
|
|
|
1505
829
|
return {
|
|
1506
|
-
query:
|
|
830
|
+
query: handle,
|
|
1507
831
|
isTemp: true,
|
|
1508
|
-
cleanup: function() { try {
|
|
832
|
+
cleanup: function() { try { handle.close(); } catch(e) {} },
|
|
1509
833
|
};
|
|
1510
834
|
}
|
|
1511
835
|
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
836
|
+
// --- Unified rewind/fork interface (adapter-agnostic) ---
|
|
837
|
+
|
|
838
|
+
async function rewindPreview(session, uuid) {
|
|
839
|
+
var sessionAdapter = getAdapterForSession(session);
|
|
840
|
+
// Adapters with rollbackThread (e.g. Codex) do chat-only rewind, no file diffs
|
|
841
|
+
if (sessionAdapter && typeof sessionAdapter.rollbackThread === "function") {
|
|
842
|
+
return { preview: { filesChanged: [] }, diffs: {}, chatOnly: true };
|
|
1518
843
|
}
|
|
844
|
+
// Claude path: use rewindFiles with dryRun
|
|
845
|
+
var result = await getOrCreateRewindQuery(session);
|
|
846
|
+
try {
|
|
847
|
+
var preview = await result.query.rewindFiles(uuid, { dryRun: true });
|
|
848
|
+
var diffs = {};
|
|
849
|
+
var changedFiles = preview.filesChanged || [];
|
|
850
|
+
for (var f = 0; f < changedFiles.length; f++) {
|
|
851
|
+
try {
|
|
852
|
+
diffs[changedFiles[f]] = require("child_process").execFileSync(
|
|
853
|
+
"git", ["diff", "HEAD", "--", changedFiles[f]],
|
|
854
|
+
{ cwd: cwd, encoding: "utf8", timeout: 5000 }
|
|
855
|
+
) || "";
|
|
856
|
+
} catch (e) { diffs[changedFiles[f]] = ""; }
|
|
857
|
+
}
|
|
858
|
+
return { preview: preview, diffs: diffs, chatOnly: false };
|
|
859
|
+
} finally {
|
|
860
|
+
if (result.isTemp) result.cleanup();
|
|
861
|
+
}
|
|
862
|
+
}
|
|
1519
863
|
|
|
1520
|
-
|
|
864
|
+
async function rewindExecuteFiles(session, uuid) {
|
|
865
|
+
var sessionAdapter = getAdapterForSession(session);
|
|
866
|
+
// Adapters with rollbackThread skip file restoration
|
|
867
|
+
if (sessionAdapter && typeof sessionAdapter.rollbackThread === "function") return;
|
|
868
|
+
// Claude path: restore files
|
|
869
|
+
var result = await getOrCreateRewindQuery(session);
|
|
1521
870
|
try {
|
|
1522
|
-
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
871
|
+
await result.query.rewindFiles(uuid, { dryRun: false });
|
|
872
|
+
} finally {
|
|
873
|
+
if (result.isTemp) result.cleanup();
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
async function rollbackConversation(session, numTurns) {
|
|
878
|
+
var sessionAdapter = getAdapterForSession(session);
|
|
879
|
+
if (sessionAdapter && typeof sessionAdapter.rollbackThread === "function") {
|
|
880
|
+
await sessionAdapter.rollbackThread(session.cliSessionId, numTurns);
|
|
881
|
+
}
|
|
882
|
+
// Claude: conversation rollback is handled by rewindFiles + local history trim
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function getAdapterForSession(session) {
|
|
886
|
+
var vendor = session.vendor || sm.defaultVendor || "claude";
|
|
887
|
+
return adapters[vendor] || adapter;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
async function forkSessionUnified(session, uuid) {
|
|
891
|
+
var sessionAdapter = getAdapterForSession(session);
|
|
892
|
+
var result = await sessionAdapter.forkSession(session.cliSessionId, { upToMessageId: uuid, dir: cwd });
|
|
893
|
+
if (!result || !result.sessionId) throw new Error("Fork returned no session id");
|
|
894
|
+
|
|
895
|
+
// Adapters with rollbackThread (e.g. Codex) use local history copy
|
|
896
|
+
if (typeof sessionAdapter.rollbackThread === "function") {
|
|
897
|
+
return { sessionId: result.sessionId, useLocalHistory: true };
|
|
898
|
+
}
|
|
899
|
+
// Claude: read history from CLI session files
|
|
900
|
+
return { sessionId: result.sessionId, useLocalHistory: false };
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
async function startQuery(session, text, images, linuxUser) {
|
|
904
|
+
// If vendor is set but adapter not ready, try lazy creation (user may have logged in)
|
|
905
|
+
if (session.vendor && !adapters[session.vendor]) {
|
|
906
|
+
var yoke = require("./yoke");
|
|
907
|
+
var lazyAdapter = yoke.lazyCreateAdapter(adapters, session.vendor, { cwd: cwd });
|
|
908
|
+
if (lazyAdapter) {
|
|
909
|
+
console.log("[sdk-bridge] Lazy adapter created for " + session.vendor);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
// If still not available after lazy check, send auth_required
|
|
913
|
+
if (session.vendor && !adapters[session.vendor]) {
|
|
914
|
+
var vendorName = session.vendor.charAt(0).toUpperCase() + session.vendor.slice(1);
|
|
915
|
+
var authUser = session.ownerId ? usersModule.findUserById(session.ownerId) : null;
|
|
916
|
+
var authLinuxUser = authUser && authUser.linuxUser ? authUser.linuxUser : null;
|
|
917
|
+
var canAutoLogin = !usersModule.isMultiUser()
|
|
918
|
+
|| !!authLinuxUser
|
|
919
|
+
|| (authUser && authUser.role === "admin");
|
|
920
|
+
sendAndRecord(session, {
|
|
921
|
+
type: "auth_required",
|
|
922
|
+
text: vendorName + " is not logged in.",
|
|
923
|
+
vendor: session.vendor,
|
|
924
|
+
loginCommand: session.vendor === "codex" ? "codex --login" : session.vendor + " login",
|
|
925
|
+
linuxUser: authLinuxUser,
|
|
926
|
+
canAutoLogin: canAutoLogin,
|
|
927
|
+
});
|
|
1527
928
|
sendAndRecord(session, { type: "done", code: 1 });
|
|
1528
|
-
sm.broadcastSessionList();
|
|
1529
929
|
return;
|
|
1530
930
|
}
|
|
931
|
+
// Select adapter based on session vendor (fallback to default)
|
|
932
|
+
var sessionAdapter = (session.vendor && adapters[session.vendor]) || adapter;
|
|
933
|
+
console.log("[sdk-bridge] startQuery: vendor=" + sessionAdapter.vendor + " session=" + session.localId + " text=" + (text || "").substring(0, 50));
|
|
934
|
+
// Remember linuxUser for auto-continue after rate limit
|
|
935
|
+
session.lastLinuxUser = linuxUser || null;
|
|
936
|
+
|
|
937
|
+
var t0 = session._queryStartTs || Date.now();
|
|
938
|
+
|
|
939
|
+
// Wait for previous worker to fully exit before spawning a new one.
|
|
940
|
+
// Without this, the new worker may try to resume the SDK session file
|
|
941
|
+
// while the old worker is still flushing it to disk, causing
|
|
942
|
+
// "no conversation found" and losing all prior context.
|
|
943
|
+
// Harmless if null (no previous worker).
|
|
944
|
+
if (session._workerExitPromise) {
|
|
945
|
+
var exitWait = session._workerExitPromise;
|
|
946
|
+
session._workerExitPromise = null;
|
|
947
|
+
await Promise.race([
|
|
948
|
+
exitWait,
|
|
949
|
+
new Promise(function(resolve) { setTimeout(resolve, 3000); }),
|
|
950
|
+
]);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Ensure Linux user project directory exists (runs in parallel with worker boot)
|
|
954
|
+
if (linuxUser) {
|
|
955
|
+
ensureLinuxUserProjectDir(linuxUser, session);
|
|
956
|
+
}
|
|
1531
957
|
|
|
1532
|
-
session.messageQueue = createMessageQueue();
|
|
1533
958
|
session.blocks = {};
|
|
1534
959
|
session.sentToolResults = {};
|
|
1535
960
|
session.activeTaskToolIds = {};
|
|
@@ -1537,106 +962,118 @@ function createSDKBridge(opts) {
|
|
|
1537
962
|
session.streamedText = false;
|
|
1538
963
|
session.responsePreview = "";
|
|
1539
964
|
|
|
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
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
if (text) {
|
|
1551
|
-
content.push({ type: "text", text: text });
|
|
965
|
+
// For in-process path, create AbortController. For worker path, the adapter
|
|
966
|
+
// handles abort internally and exposes it via handle.abort().
|
|
967
|
+
if (!linuxUser) {
|
|
968
|
+
session.abortController = new AbortController();
|
|
1552
969
|
}
|
|
1553
970
|
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
message: { role: "user", content: content },
|
|
1557
|
-
});
|
|
1558
|
-
|
|
1559
|
-
session.abortController = new AbortController();
|
|
1560
|
-
|
|
1561
|
-
var queryOptions = {
|
|
1562
|
-
cwd: cwd,
|
|
971
|
+
// Build Claude-specific adapter options
|
|
972
|
+
var claudeOpts = {
|
|
1563
973
|
settingSources: ["user", "project", "local"],
|
|
1564
974
|
includePartialMessages: true,
|
|
1565
975
|
enableFileCheckpointing: true,
|
|
1566
976
|
extraArgs: { "replay-user-messages": null },
|
|
1567
|
-
abortController: session.abortController,
|
|
1568
977
|
promptSuggestions: true,
|
|
1569
978
|
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
979
|
};
|
|
1578
980
|
|
|
1579
981
|
// Per-loop settings override global defaults when present
|
|
1580
982
|
var ls = session.loopSettings || {};
|
|
1581
983
|
|
|
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
984
|
if (sm.currentBetas && sm.currentBetas.length > 0) {
|
|
1591
|
-
|
|
985
|
+
claudeOpts.betas = sm.currentBetas;
|
|
1592
986
|
}
|
|
1593
|
-
|
|
1594
987
|
var thinkingMode = ls.thinking || sm.currentThinking;
|
|
1595
988
|
if (thinkingMode === "disabled") {
|
|
1596
|
-
|
|
989
|
+
claudeOpts.thinking = { type: "disabled" };
|
|
1597
990
|
} else if (thinkingMode === "budget") {
|
|
1598
991
|
var budgetTokens = ls.thinkingBudget || sm.currentThinkingBudget;
|
|
1599
|
-
if (budgetTokens)
|
|
992
|
+
if (budgetTokens) claudeOpts.thinking = { type: "enabled", budgetTokens: budgetTokens };
|
|
1600
993
|
}
|
|
1601
994
|
|
|
1602
995
|
if (ls.permissionMode) {
|
|
1603
|
-
// Will be applied below, store for later
|
|
1604
996
|
session._loopPermissionMode = ls.permissionMode;
|
|
1605
997
|
}
|
|
1606
998
|
|
|
1607
999
|
// Pass through any extra SDK settings from LOOP.json
|
|
1608
1000
|
if (ls.disableAllHooks !== undefined) {
|
|
1609
|
-
|
|
1001
|
+
claudeOpts.settings = Object.assign({}, claudeOpts.settings || {}, { disableAllHooks: ls.disableAllHooks });
|
|
1610
1002
|
}
|
|
1611
1003
|
|
|
1612
1004
|
if (dangerouslySkipPermissions) {
|
|
1613
|
-
|
|
1005
|
+
claudeOpts.allowDangerouslySkipPermissions = true;
|
|
1614
1006
|
}
|
|
1615
|
-
// Pass permissionMode in queryOptions at creation time to avoid race condition
|
|
1616
1007
|
var modeToApply = session._loopPermissionMode || (session.acceptEditsAfterStart ? "acceptEdits" : sm.currentPermissionMode);
|
|
1617
1008
|
if (session.acceptEditsAfterStart) delete session.acceptEditsAfterStart;
|
|
1618
1009
|
if (modeToApply && modeToApply !== "default") {
|
|
1619
|
-
|
|
1010
|
+
claudeOpts.permissionMode = modeToApply;
|
|
1011
|
+
}
|
|
1012
|
+
if (session.cliSessionId && session.lastRewindUuid) {
|
|
1013
|
+
claudeOpts.resumeSessionAt = session.lastRewindUuid;
|
|
1014
|
+
delete session.lastRewindUuid;
|
|
1015
|
+
sm.saveSessionFile(session);
|
|
1620
1016
|
}
|
|
1621
1017
|
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1018
|
+
// Pass linuxUser to adapter for worker-based queries
|
|
1019
|
+
if (linuxUser) {
|
|
1020
|
+
claudeOpts.linuxUser = linuxUser;
|
|
1021
|
+
claudeOpts.singleTurn = !!session.singleTurn;
|
|
1022
|
+
claudeOpts.originalHome = require("./config").REAL_HOME || null;
|
|
1023
|
+
claudeOpts.projectPath = session.cwd || null;
|
|
1024
|
+
claudeOpts._perfT0 = t0;
|
|
1025
|
+
// Pass previous worker state for reuse
|
|
1026
|
+
if (session._adapterWorkerState) {
|
|
1027
|
+
claudeOpts._workerState = session._adapterWorkerState;
|
|
1028
|
+
session._adapterWorkerState = null;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// Use vendor-specific model: if session vendor differs from default, use that vendor's default model
|
|
1033
|
+
var queryModel = ls.model || sm.currentModel || undefined;
|
|
1034
|
+
if (session.vendor && session.vendor !== (adapter && adapter.vendor)) {
|
|
1035
|
+
var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[session.vendor]) || [];
|
|
1036
|
+
if (vendorModels.length > 0 && queryModel && vendorModels.indexOf(queryModel) === -1) {
|
|
1037
|
+
queryModel = vendorModels[0];
|
|
1629
1038
|
}
|
|
1630
1039
|
}
|
|
1631
1040
|
|
|
1041
|
+
var codexConfig = getCodexConfig(sm);
|
|
1042
|
+
var queryOpts = {
|
|
1043
|
+
cwd: cwd,
|
|
1044
|
+
model: queryModel,
|
|
1045
|
+
effort: ls.effort || sm.currentEffort || undefined,
|
|
1046
|
+
toolServers: mergeMcpServers(mcpServers, getRemoteMcpServers) || undefined,
|
|
1047
|
+
resumeSessionId: session.cliSessionId || undefined,
|
|
1048
|
+
abortController: linuxUser ? undefined : session.abortController,
|
|
1049
|
+
canUseTool: function(toolName, input, toolOpts) {
|
|
1050
|
+
return handleCanUseTool(session, toolName, input, toolOpts);
|
|
1051
|
+
},
|
|
1052
|
+
onElicitation: function(request, elicitOpts) {
|
|
1053
|
+
return handleElicitation(session, request, elicitOpts);
|
|
1054
|
+
},
|
|
1055
|
+
adapterOptions: {
|
|
1056
|
+
CLAUDE: claudeOpts,
|
|
1057
|
+
CODEX: {
|
|
1058
|
+
// Always use "never" (auto-approve) because Clay handles tool
|
|
1059
|
+
// permissions via its own UI (checkToolWhitelist + handleCanUseTool).
|
|
1060
|
+
// Codex's native approval prompts are terminal-based and cannot be
|
|
1061
|
+
// relayed through Clay's web UI, causing MCP tool calls to hang.
|
|
1062
|
+
approvalPolicy: "never",
|
|
1063
|
+
sandboxMode: codexConfig.sandbox,
|
|
1064
|
+
webSearchMode: codexConfig.webSearch,
|
|
1065
|
+
},
|
|
1066
|
+
},
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
var handle;
|
|
1070
|
+
console.log("[sdk-bridge] calling adapter.createQuery... vendor=" + sessionAdapter.vendor);
|
|
1632
1071
|
try {
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
options: queryOptions,
|
|
1636
|
-
});
|
|
1072
|
+
handle = await sessionAdapter.createQuery(queryOpts);
|
|
1073
|
+
console.log("[sdk-bridge] createQuery returned handle, vendor=" + sessionAdapter.vendor);
|
|
1637
1074
|
} catch (e) {
|
|
1638
1075
|
console.error("[sdk-bridge] Failed to create query for session " + session.localId + ":", e.message || e);
|
|
1639
|
-
console.error("[sdk-bridge] cliSessionId:", session.cliSessionId, "resume:", !!
|
|
1076
|
+
console.error("[sdk-bridge] cliSessionId:", session.cliSessionId, "resume:", !!session.cliSessionId);
|
|
1640
1077
|
console.error("[sdk-bridge] Stack:", e.stack || "(no stack)");
|
|
1641
1078
|
session.isProcessing = false;
|
|
1642
1079
|
onProcessingChanged();
|
|
@@ -1649,12 +1086,38 @@ function createSDKBridge(opts) {
|
|
|
1649
1086
|
return;
|
|
1650
1087
|
}
|
|
1651
1088
|
|
|
1089
|
+
// Store adapter worker state for reuse on next query
|
|
1090
|
+
if (handle._adapterState) {
|
|
1091
|
+
session._adapterWorkerState = handle._adapterState;
|
|
1092
|
+
// Keep session.worker reference for external code (sessions.js, project.js)
|
|
1093
|
+
// that needs to kill the worker on session destroy.
|
|
1094
|
+
if (handle._adapterState.worker) {
|
|
1095
|
+
session.worker = handle._adapterState.worker;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// For worker path, create an abortController wrapper that delegates to handle.abort()
|
|
1100
|
+
if (linuxUser) {
|
|
1101
|
+
session.abortController = {
|
|
1102
|
+
abort: function() { handle.abort(); },
|
|
1103
|
+
signal: { aborted: false, addEventListener: function() {} },
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Store QueryHandle on session for iteration and control.
|
|
1108
|
+
session.queryInstance = handle;
|
|
1109
|
+
|
|
1110
|
+
// Push initial user message through the QueryHandle
|
|
1111
|
+
console.log("[sdk-bridge] pushing initial message via handle.pushMessage...");
|
|
1112
|
+
handle.pushMessage(text, images);
|
|
1113
|
+
console.log("[sdk-bridge] pushMessage done, starting processQueryStream...");
|
|
1114
|
+
|
|
1652
1115
|
// For single-turn sessions (Ralph Loop), end the message queue so the SDK
|
|
1653
1116
|
// query finishes after processing the one message. Without this, the query
|
|
1654
1117
|
// stream stays open forever waiting for more messages, and onQueryComplete
|
|
1655
1118
|
// never fires.
|
|
1656
1119
|
if (session.singleTurn) {
|
|
1657
|
-
|
|
1120
|
+
handle.endInput();
|
|
1658
1121
|
}
|
|
1659
1122
|
|
|
1660
1123
|
session.lastActivityAt = Date.now();
|
|
@@ -1663,28 +1126,10 @@ function createSDKBridge(opts) {
|
|
|
1663
1126
|
}
|
|
1664
1127
|
|
|
1665
1128
|
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
1129
|
session.lastActivityAt = Date.now();
|
|
1683
|
-
// Route through
|
|
1684
|
-
if (session.
|
|
1685
|
-
session.
|
|
1686
|
-
} else {
|
|
1687
|
-
session.messageQueue.push(userMsg);
|
|
1130
|
+
// Route through QueryHandle (works for both in-process and worker paths)
|
|
1131
|
+
if (session.queryInstance && typeof session.queryInstance.pushMessage === "function") {
|
|
1132
|
+
session.queryInstance.pushMessage(text, images);
|
|
1688
1133
|
}
|
|
1689
1134
|
}
|
|
1690
1135
|
|
|
@@ -1731,74 +1176,126 @@ function createSDKBridge(opts) {
|
|
|
1731
1176
|
return text;
|
|
1732
1177
|
}
|
|
1733
1178
|
|
|
1734
|
-
//
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1179
|
+
// Detect which vendor binaries are installed for this user.
|
|
1180
|
+
// In multi-user mode, runs checks as the specific Linux user.
|
|
1181
|
+
function detectInstalledVendors(linuxUser) {
|
|
1182
|
+
var execSync = require("child_process").execSync;
|
|
1183
|
+
var fs = require("fs");
|
|
1184
|
+
var path = require("path");
|
|
1185
|
+
var result = [];
|
|
1740
1186
|
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1187
|
+
function tryExec(cmd) {
|
|
1188
|
+
try {
|
|
1189
|
+
if (linuxUser) {
|
|
1190
|
+
execSync("su - " + linuxUser + " -c " + JSON.stringify(cmd), { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1191
|
+
} else {
|
|
1192
|
+
execSync(cmd, { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1193
|
+
}
|
|
1194
|
+
return true;
|
|
1195
|
+
} catch (e) {
|
|
1196
|
+
return false;
|
|
1751
1197
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Claude: check if binary is in PATH
|
|
1201
|
+
if (tryExec("which claude")) result.push("claude");
|
|
1202
|
+
|
|
1203
|
+
// Codex: check bundled binary or PATH
|
|
1204
|
+
var codexBin = path.join(__dirname, "../node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/codex/codex");
|
|
1205
|
+
if (fs.existsSync(codexBin) || tryExec("which codex")) result.push("codex");
|
|
1206
|
+
|
|
1207
|
+
return result;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// SDK warmup: initialize all available adapters and collect models.
|
|
1211
|
+
// The default adapter is initialized first for slash_commands and skills.
|
|
1212
|
+
// Passes linuxUser to adapter for worker-based warmup when OS isolation is needed.
|
|
1213
|
+
async function warmup(linuxUser) {
|
|
1214
|
+
var defaultVendor = adapter ? adapter.vendor : "claude";
|
|
1215
|
+
sm.defaultVendor = defaultVendor;
|
|
1216
|
+
|
|
1217
|
+
// Initialize default adapter first (provides skills, slash commands, etc.)
|
|
1218
|
+
if (adapter) {
|
|
1219
|
+
try {
|
|
1220
|
+
var result = await adapter.init({
|
|
1221
|
+
cwd: cwd,
|
|
1222
|
+
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
1223
|
+
linuxUser: linuxUser || undefined,
|
|
1224
|
+
clayPort: clayPort,
|
|
1225
|
+
clayTls: clayTls,
|
|
1226
|
+
clayAuthToken: clayAuthToken,
|
|
1227
|
+
slug: slug,
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
var fsSkills = discoverSkillDirs();
|
|
1231
|
+
sm.skillNames = mergeSkills(result.skills, fsSkills);
|
|
1232
|
+
if (result.slashCommands) {
|
|
1233
|
+
var seen = new Set();
|
|
1234
|
+
var combined = [];
|
|
1235
|
+
var all = result.slashCommands.concat(Array.from(sm.skillNames));
|
|
1236
|
+
for (var k = 0; k < all.length; k++) {
|
|
1237
|
+
if (!seen.has(all[k])) {
|
|
1238
|
+
seen.add(all[k]);
|
|
1239
|
+
combined.push(all[k]);
|
|
1770
1240
|
}
|
|
1771
|
-
sm.slashCommands = combined;
|
|
1772
|
-
send({ type: "slash_commands", commands: sm.slashCommands });
|
|
1773
|
-
}
|
|
1774
|
-
if (msg.model) {
|
|
1775
|
-
sm.currentModel = msg.model;
|
|
1776
1241
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1242
|
+
sm.slashCommands = combined;
|
|
1243
|
+
sm.setSlashCommandsForVendor(defaultVendor, combined);
|
|
1244
|
+
send({ type: "slash_commands", commands: combined, vendor: defaultVendor });
|
|
1245
|
+
}
|
|
1246
|
+
if (result.defaultModel) {
|
|
1247
|
+
sm.currentModel = sm.currentModel || sm._savedDefaultModel || result.defaultModel;
|
|
1248
|
+
}
|
|
1249
|
+
sm.availableModels = result.models || [];
|
|
1250
|
+
// Store per-vendor models and capabilities
|
|
1251
|
+
sm.modelsByVendor = sm.modelsByVendor || {};
|
|
1252
|
+
sm.modelsByVendor[defaultVendor] = result.models || [];
|
|
1253
|
+
sm.capabilitiesByVendor = sm.capabilitiesByVendor || {};
|
|
1254
|
+
sm.capabilitiesByVendor[defaultVendor] = result.capabilities || {};
|
|
1255
|
+
} catch (e) {
|
|
1256
|
+
if (e && e.name !== "AbortError" && !(e.message && e.message.indexOf("aborted") !== -1)) {
|
|
1257
|
+
send({ type: "error", text: "Failed to load " + defaultVendor + " SDK: " + (e.message || e) });
|
|
1785
1258
|
}
|
|
1786
1259
|
}
|
|
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
|
-
}
|
|
1791
1260
|
}
|
|
1261
|
+
|
|
1262
|
+
// Initialize other adapters in parallel (skip if already have models cached)
|
|
1263
|
+
sm.modelsByVendor = sm.modelsByVendor || {};
|
|
1264
|
+
var otherVendors = Object.keys(adapters).filter(function(v) {
|
|
1265
|
+
return v !== defaultVendor && !sm.modelsByVendor[v];
|
|
1266
|
+
});
|
|
1267
|
+
for (var i = 0; i < otherVendors.length; i++) {
|
|
1268
|
+
(function(v) {
|
|
1269
|
+
adapters[v].init({ cwd: cwd, clayPort: clayPort, clayTls: clayTls, clayAuthToken: clayAuthToken, slug: slug }).then(function(r) {
|
|
1270
|
+
sm.modelsByVendor[v] = r.models || [];
|
|
1271
|
+
sm.capabilitiesByVendor[v] = r.capabilities || {};
|
|
1272
|
+
if (r.slashCommands) sm.setSlashCommandsForVendor(v, r.slashCommands);
|
|
1273
|
+
}).catch(function(e) {
|
|
1274
|
+
console.error("[sdk-bridge] warmup: " + v + " init failed:", e.message || e);
|
|
1275
|
+
});
|
|
1276
|
+
})(otherVendors[i]);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// Detect installed vendors per-user (binary existence check)
|
|
1280
|
+
sm.installedVendors = detectInstalledVendors(linuxUser);
|
|
1281
|
+
sm.availableVendors = Object.keys(adapters);
|
|
1282
|
+
|
|
1283
|
+
// Send initial state to client
|
|
1284
|
+
send({
|
|
1285
|
+
type: "model_info",
|
|
1286
|
+
model: sm.currentModel || "",
|
|
1287
|
+
models: sm.availableModels || [],
|
|
1288
|
+
vendor: defaultVendor,
|
|
1289
|
+
availableVendors: sm.availableVendors,
|
|
1290
|
+
installedVendors: sm.installedVendors,
|
|
1291
|
+
});
|
|
1792
1292
|
}
|
|
1793
1293
|
|
|
1794
1294
|
async function setModel(session, model) {
|
|
1795
|
-
if (session.worker) {
|
|
1796
|
-
session.worker.send({ type: "set_model", model: model });
|
|
1797
|
-
return;
|
|
1798
|
-
}
|
|
1799
1295
|
if (!session.queryInstance) {
|
|
1800
1296
|
// No active query — just store the model for next startQuery
|
|
1801
1297
|
sm.currentModel = model;
|
|
1298
|
+
// Don't send vendor here: session vendor not yet bound, let client keep its selection
|
|
1802
1299
|
send({ type: "model_info", model: model, models: sm.availableModels || [] });
|
|
1803
1300
|
send({ type: "config_state", model: sm.currentModel, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
1804
1301
|
return;
|
|
@@ -1806,7 +1303,8 @@ function createSDKBridge(opts) {
|
|
|
1806
1303
|
try {
|
|
1807
1304
|
await session.queryInstance.setModel(model);
|
|
1808
1305
|
sm.currentModel = model;
|
|
1809
|
-
|
|
1306
|
+
var sessionVendor = session.vendor || (adapter && adapter.vendor) || "claude";
|
|
1307
|
+
send({ type: "model_info", model: model, models: sm.availableModels || [], vendor: sessionVendor });
|
|
1810
1308
|
send({ type: "config_state", model: sm.currentModel, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
1811
1309
|
} catch (e) {
|
|
1812
1310
|
send({ type: "error", text: "Failed to switch model: " + (e.message || e) });
|
|
@@ -1814,26 +1312,20 @@ function createSDKBridge(opts) {
|
|
|
1814
1312
|
}
|
|
1815
1313
|
|
|
1816
1314
|
async function setEffort(session, effort) {
|
|
1817
|
-
if (session.worker) {
|
|
1818
|
-
session.worker.send({ type: "set_effort", effort: effort });
|
|
1819
|
-
return;
|
|
1820
|
-
}
|
|
1821
1315
|
if (!session.queryInstance) {
|
|
1822
1316
|
sm.currentEffort = effort;
|
|
1823
1317
|
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [] });
|
|
1824
1318
|
return;
|
|
1825
1319
|
}
|
|
1826
|
-
//
|
|
1827
|
-
|
|
1320
|
+
// Route through QueryHandle (works for both in-process and worker paths)
|
|
1321
|
+
if (typeof session.queryInstance.setEffort === "function") {
|
|
1322
|
+
await session.queryInstance.setEffort(effort);
|
|
1323
|
+
}
|
|
1828
1324
|
sm.currentEffort = effort;
|
|
1829
1325
|
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [] });
|
|
1830
1326
|
}
|
|
1831
1327
|
|
|
1832
1328
|
async function setPermissionMode(session, mode) {
|
|
1833
|
-
if (session.worker) {
|
|
1834
|
-
session.worker.send({ type: "set_permission_mode", mode: mode });
|
|
1835
|
-
return;
|
|
1836
|
-
}
|
|
1837
1329
|
if (!session.queryInstance) {
|
|
1838
1330
|
// No active query — just store the mode for next startQuery
|
|
1839
1331
|
sm.currentPermissionMode = mode;
|
|
@@ -1841,6 +1333,7 @@ function createSDKBridge(opts) {
|
|
|
1841
1333
|
return;
|
|
1842
1334
|
}
|
|
1843
1335
|
try {
|
|
1336
|
+
// Route through QueryHandle (works for both in-process and worker paths)
|
|
1844
1337
|
await session.queryInstance.setPermissionMode(mode);
|
|
1845
1338
|
sm.currentPermissionMode = mode;
|
|
1846
1339
|
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
@@ -1853,12 +1346,9 @@ function createSDKBridge(opts) {
|
|
|
1853
1346
|
var session = sm.getActiveSession();
|
|
1854
1347
|
if (!session) return;
|
|
1855
1348
|
session.taskStopRequested = true;
|
|
1856
|
-
if (session.worker) {
|
|
1857
|
-
session.worker.send({ type: "stop_task", taskId: taskId });
|
|
1858
|
-
return;
|
|
1859
|
-
}
|
|
1860
1349
|
if (!session.queryInstance) return;
|
|
1861
1350
|
try {
|
|
1351
|
+
// Route through QueryHandle (works for both in-process and worker paths)
|
|
1862
1352
|
await session.queryInstance.stopTask(taskId);
|
|
1863
1353
|
} catch (e) {
|
|
1864
1354
|
console.error("[sdk-bridge] stopTask error:", e.message);
|
|
@@ -1874,16 +1364,7 @@ function createSDKBridge(opts) {
|
|
|
1874
1364
|
// Creates a mention session that can be reused across multiple mentions
|
|
1875
1365
|
// within a conversation flow (session continuity).
|
|
1876
1366
|
async function createMentionSession(opts) {
|
|
1877
|
-
// 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();
|
|
1367
|
+
// opts: { vendor, claudeMd, initialContext, initialMessage, onDelta, onDone, onError, onActivity }
|
|
1887
1368
|
var abortController = new AbortController();
|
|
1888
1369
|
|
|
1889
1370
|
// Current response callbacks (swapped on each pushMessage)
|
|
@@ -1896,114 +1377,89 @@ function createSDKBridge(opts) {
|
|
|
1896
1377
|
var mentionBlocks = {};
|
|
1897
1378
|
var alive = true;
|
|
1898
1379
|
|
|
1899
|
-
|
|
1380
|
+
// Use the mate's vendor adapter if specified, otherwise default
|
|
1381
|
+
var mentionAdapter = (opts.vendor && adapters[opts.vendor]) || adapter;
|
|
1382
|
+
|
|
1383
|
+
var handle;
|
|
1900
1384
|
try {
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1385
|
+
handle = await mentionAdapter.createQuery({
|
|
1386
|
+
cwd: cwd,
|
|
1387
|
+
systemPrompt: opts.claudeMd,
|
|
1388
|
+
model: opts.model || undefined,
|
|
1389
|
+
toolServers: opts.includeMcpServers ? (mergeMcpServers(mcpServers, getRemoteMcpServers) || undefined) : undefined,
|
|
1390
|
+
abortController: abortController,
|
|
1391
|
+
canUseTool: opts.canUseTool || function (toolName, input) {
|
|
1392
|
+
var whitelisted = checkToolWhitelist(toolName, input);
|
|
1393
|
+
if (whitelisted) {
|
|
1394
|
+
return Promise.resolve(whitelisted);
|
|
1395
|
+
}
|
|
1396
|
+
return Promise.resolve({
|
|
1397
|
+
behavior: "deny",
|
|
1398
|
+
message: "Read-only access. You cannot make changes via @mention.",
|
|
1399
|
+
});
|
|
1400
|
+
},
|
|
1401
|
+
adapterOptions: {
|
|
1402
|
+
CLAUDE: {
|
|
1403
|
+
settingSources: ["user"],
|
|
1404
|
+
includePartialMessages: true,
|
|
1916
1405
|
},
|
|
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,
|
|
1406
|
+
},
|
|
1926
1407
|
});
|
|
1927
1408
|
} catch (e) {
|
|
1928
1409
|
opts.onError("Failed to create mention query: " + (e.message || e));
|
|
1929
1410
|
return null;
|
|
1930
1411
|
}
|
|
1412
|
+
var query = handle;
|
|
1931
1413
|
|
|
1932
1414
|
// Push the initial message (context + question, with optional images)
|
|
1933
1415
|
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
|
-
});
|
|
1416
|
+
handle.pushMessage(initialPrompt, opts.initialImages || null);
|
|
1948
1417
|
|
|
1949
|
-
// Background stream processing loop
|
|
1418
|
+
// Background stream processing loop (consumes flattened yokeType events)
|
|
1950
1419
|
(async function () {
|
|
1951
1420
|
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
|
-
}
|
|
1421
|
+
for await (var msg of query) {
|
|
1422
|
+
// Track content blocks for activity reporting
|
|
1423
|
+
if (msg.yokeType === "thinking_start") {
|
|
1424
|
+
mentionBlocks[msg.blockId] = { type: "thinking" };
|
|
1425
|
+
if (currentOnActivity) currentOnActivity("thinking");
|
|
1426
|
+
} else if (msg.yokeType === "tool_start") {
|
|
1427
|
+
mentionBlocks[msg.blockId] = { type: "tool_use", name: msg.toolName, inputJson: "" };
|
|
1428
|
+
var toolLabel = msg.toolName;
|
|
1429
|
+
if (toolLabel === "Read") toolLabel = "Reading file...";
|
|
1430
|
+
else if (toolLabel === "Grep") toolLabel = "Searching code...";
|
|
1431
|
+
else if (toolLabel === "Glob") toolLabel = "Finding files...";
|
|
1432
|
+
if (currentOnActivity) currentOnActivity(toolLabel);
|
|
1433
|
+
} else if (msg.yokeType === "text_start") {
|
|
1434
|
+
mentionBlocks[msg.blockId] = { type: "text" };
|
|
1435
|
+
|
|
1436
|
+
} else if (msg.yokeType === "text_delta" && typeof msg.text === "string") {
|
|
1437
|
+
responseStreamedText = true;
|
|
1438
|
+
responseFullText += msg.text;
|
|
1439
|
+
if (currentOnActivity) currentOnActivity(null);
|
|
1440
|
+
if (currentOnDelta) currentOnDelta(msg.text);
|
|
1441
|
+
} else if (msg.yokeType === "tool_input_delta" && mentionBlocks[msg.blockId]) {
|
|
1442
|
+
mentionBlocks[msg.blockId].inputJson += msg.partialJson;
|
|
1443
|
+
|
|
1444
|
+
} else if (msg.yokeType === "block_stop") {
|
|
1445
|
+
var blk = mentionBlocks[msg.blockId];
|
|
1446
|
+
if (blk && blk.type === "tool_use") {
|
|
1447
|
+
var toolInput = {};
|
|
1448
|
+
try { toolInput = JSON.parse(blk.inputJson); } catch (e) {}
|
|
1449
|
+
if (blk.name === "Read" && toolInput.file_path) {
|
|
1450
|
+
var fname = toolInput.file_path.split(/[/\\]/).pop();
|
|
1451
|
+
if (currentOnActivity) currentOnActivity("Reading " + fname + "...");
|
|
1452
|
+
} else if (blk.name === "Grep" && toolInput.pattern) {
|
|
1453
|
+
if (currentOnActivity) currentOnActivity("Searching: " + toolInput.pattern.substring(0, 30) + "...");
|
|
1454
|
+
} else if (blk.name === "Glob" && toolInput.pattern) {
|
|
1455
|
+
if (currentOnActivity) currentOnActivity("Finding: " + toolInput.pattern.substring(0, 30) + "...");
|
|
2000
1456
|
}
|
|
2001
|
-
delete mentionBlocks[evt.index];
|
|
2002
1457
|
}
|
|
1458
|
+
delete mentionBlocks[msg.blockId];
|
|
2003
1459
|
|
|
2004
|
-
} else if (
|
|
1460
|
+
} else if (msg.yokeType === "message" && msg.messageRole === "assistant" && !responseStreamedText && msg.content) {
|
|
2005
1461
|
// Fallback: if text was not streamed via deltas, extract from assistant message
|
|
2006
|
-
var content =
|
|
1462
|
+
var content = msg.content;
|
|
2007
1463
|
if (Array.isArray(content)) {
|
|
2008
1464
|
for (var ci = 0; ci < content.length; ci++) {
|
|
2009
1465
|
if (content[ci].type === "text" && content[ci].text) {
|
|
@@ -2012,7 +1468,8 @@ function createSDKBridge(opts) {
|
|
|
2012
1468
|
}
|
|
2013
1469
|
}
|
|
2014
1470
|
}
|
|
2015
|
-
|
|
1471
|
+
|
|
1472
|
+
} else if (msg.yokeType === "result") {
|
|
2016
1473
|
// One response complete. Signal done and reset for next message.
|
|
2017
1474
|
if (currentOnActivity) currentOnActivity(null);
|
|
2018
1475
|
var doneRef = currentOnDone;
|
|
@@ -2054,27 +1511,14 @@ function createSDKBridge(opts) {
|
|
|
2054
1511
|
mentionBlocks = {};
|
|
2055
1512
|
responseFullText = "";
|
|
2056
1513
|
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
|
-
});
|
|
1514
|
+
handle.pushMessage(text, images || null);
|
|
2071
1515
|
},
|
|
2072
1516
|
abort: function () {
|
|
2073
1517
|
try { abortController.abort(); } catch (e) {}
|
|
2074
1518
|
},
|
|
2075
1519
|
close: function () {
|
|
2076
1520
|
alive = false;
|
|
2077
|
-
try {
|
|
1521
|
+
try { handle.close(); } catch (e) {}
|
|
2078
1522
|
},
|
|
2079
1523
|
isAlive: function () { return alive; },
|
|
2080
1524
|
};
|
|
@@ -2088,6 +1532,10 @@ function createSDKBridge(opts) {
|
|
|
2088
1532
|
handleElicitation: handleElicitation,
|
|
2089
1533
|
processQueryStream: processQueryStream,
|
|
2090
1534
|
getOrCreateRewindQuery: getOrCreateRewindQuery,
|
|
1535
|
+
rewindPreview: rewindPreview,
|
|
1536
|
+
rewindExecuteFiles: rewindExecuteFiles,
|
|
1537
|
+
rollbackConversation: rollbackConversation,
|
|
1538
|
+
forkSession: forkSessionUnified,
|
|
2091
1539
|
startQuery: startQuery,
|
|
2092
1540
|
pushMessage: pushMessage,
|
|
2093
1541
|
setModel: setModel,
|