clay-server 2.31.0 → 2.32.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser-mcp-server.js +32 -44
- package/lib/debate-mcp-server.js +14 -31
- package/lib/mcp-local.js +31 -1
- package/lib/project-connection.js +4 -2
- package/lib/project-filesystem.js +47 -1
- package/lib/project-http.js +75 -8
- package/lib/project-mcp.js +4 -0
- package/lib/project-sessions.js +88 -51
- package/lib/project-user-message.js +12 -7
- package/lib/project.js +204 -90
- package/lib/public/app.js +123 -448
- package/lib/public/codex-avatar.png +0 -0
- package/lib/public/css/debate.css +3 -2
- package/lib/public/css/filebrowser.css +91 -1
- package/lib/public/css/icon-strip.css +21 -5
- package/lib/public/css/input.css +181 -100
- package/lib/public/css/mates.css +43 -0
- package/lib/public/css/mention.css +48 -4
- package/lib/public/css/menus.css +1 -1
- package/lib/public/css/messages.css +2 -0
- package/lib/public/css/notifications-center.css +19 -0
- package/lib/public/index.html +46 -24
- package/lib/public/modules/app-connection.js +138 -37
- package/lib/public/modules/app-cursors.js +18 -17
- package/lib/public/modules/app-debate-ui.js +9 -9
- package/lib/public/modules/app-dm.js +170 -131
- package/lib/public/modules/app-favicon.js +28 -26
- package/lib/public/modules/app-header.js +79 -68
- package/lib/public/modules/app-home-hub.js +55 -47
- package/lib/public/modules/app-loop-ui.js +34 -18
- package/lib/public/modules/app-loop-wizard.js +6 -6
- package/lib/public/modules/app-messages.js +195 -152
- package/lib/public/modules/app-misc.js +23 -12
- package/lib/public/modules/app-notifications.js +97 -3
- package/lib/public/modules/app-panels.js +203 -49
- package/lib/public/modules/app-projects.js +159 -150
- package/lib/public/modules/app-rate-limit.js +5 -4
- package/lib/public/modules/app-rendering.js +149 -101
- package/lib/public/modules/app-skills-install.js +4 -4
- package/lib/public/modules/context-sources.js +12 -41
- package/lib/public/modules/dom-refs.js +21 -0
- package/lib/public/modules/filebrowser.js +173 -2
- package/lib/public/modules/input.js +86 -0
- package/lib/public/modules/mate-sidebar.js +38 -0
- package/lib/public/modules/mention.js +24 -6
- package/lib/public/modules/scheduler.js +1 -1
- package/lib/public/modules/sidebar-mates.js +66 -34
- package/lib/public/modules/sidebar-mobile.js +34 -30
- package/lib/public/modules/sidebar-projects.js +60 -57
- package/lib/public/modules/sidebar-sessions.js +75 -69
- package/lib/public/modules/sidebar.js +12 -20
- package/lib/public/modules/skills.js +8 -9
- package/lib/public/modules/sticky-notes.js +1 -2
- package/lib/public/modules/store.js +9 -2
- package/lib/public/modules/stt.js +4 -1
- package/lib/public/modules/tools.js +14 -9
- package/lib/sdk-bridge.js +511 -1113
- package/lib/sdk-message-processor.js +123 -134
- package/lib/sdk-worker.js +4 -0
- package/lib/server-dm.js +1 -0
- package/lib/server.js +86 -1
- package/lib/sessions.js +47 -36
- package/lib/ws-schema.js +2 -0
- package/lib/yoke/adapters/claude-worker.js +559 -0
- package/lib/yoke/adapters/claude.js +1418 -0
- package/lib/yoke/adapters/codex.js +968 -0
- package/lib/yoke/adapters/gemini.js +668 -0
- package/lib/yoke/codex-app-server.js +307 -0
- package/lib/yoke/index.js +199 -0
- package/lib/yoke/instructions.js +62 -0
- package/lib/yoke/interface.js +92 -0
- package/lib/yoke/mcp-bridge-server.js +294 -0
- package/lib/yoke/package.json +7 -0
- package/package.json +3 -1
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
// sdk-worker.js — Standalone worker process for OS-level user isolation.
|
|
2
|
+
// Runs as a target Linux user, loads the Claude Agent SDK, and communicates
|
|
3
|
+
// with the main Clay daemon over a Unix domain socket using JSON lines.
|
|
4
|
+
//
|
|
5
|
+
// Usage: node sdk-worker.js <socket-path>
|
|
6
|
+
|
|
7
|
+
// Force IPv4-only for all child processes (including SDK CLI subprocess).
|
|
8
|
+
// Without this, Node 22+ happy eyeballs tries IPv6 first (10s timeout on
|
|
9
|
+
// servers without IPv6 outbound), causing massive cold-start delays.
|
|
10
|
+
process.env.NODE_OPTIONS = (process.env.NODE_OPTIONS || "") + " --dns-result-order=ipv4first --no-network-family-autoselection";
|
|
11
|
+
|
|
12
|
+
// Early diagnostic — writes directly to fd 2 to ensure output even if pipes close fast
|
|
13
|
+
var _workerBootTs = Date.now();
|
|
14
|
+
try { require("fs").writeSync(2, "[sdk-worker] BOOT pid=" + process.pid + " uid=" + (typeof process.getuid === "function" ? process.getuid() : "?") + " argv=" + process.argv.slice(1).join(" ") + " bootTs=" + _workerBootTs + "\n"); } catch (e) {}
|
|
15
|
+
|
|
16
|
+
var net = require("net");
|
|
17
|
+
var crypto = require("crypto");
|
|
18
|
+
var path = require("path");
|
|
19
|
+
|
|
20
|
+
var socketPath = process.argv[2];
|
|
21
|
+
if (!socketPath) {
|
|
22
|
+
console.error("[sdk-worker] Missing socket path argument");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// --- State ---
|
|
27
|
+
var sdkModule = null;
|
|
28
|
+
var queryInstance = null;
|
|
29
|
+
var messageQueue = null;
|
|
30
|
+
var abortController = null;
|
|
31
|
+
var pendingPermissions = {}; // requestId -> resolve
|
|
32
|
+
var pendingAskUser = {}; // toolUseId -> resolve
|
|
33
|
+
var pendingElicitations = {}; // requestId -> resolve
|
|
34
|
+
var conn = null;
|
|
35
|
+
var buffer = "";
|
|
36
|
+
|
|
37
|
+
// --- Message queue (same implementation as sdk-bridge.js) ---
|
|
38
|
+
function createMessageQueue() {
|
|
39
|
+
var queue = [];
|
|
40
|
+
var waiting = null;
|
|
41
|
+
var ended = false;
|
|
42
|
+
return {
|
|
43
|
+
push: function(msg) {
|
|
44
|
+
if (waiting) {
|
|
45
|
+
var resolve = waiting;
|
|
46
|
+
waiting = null;
|
|
47
|
+
resolve({ value: msg, done: false });
|
|
48
|
+
} else {
|
|
49
|
+
queue.push(msg);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
end: function() {
|
|
53
|
+
ended = true;
|
|
54
|
+
if (waiting) {
|
|
55
|
+
var resolve = waiting;
|
|
56
|
+
waiting = null;
|
|
57
|
+
resolve({ value: undefined, done: true });
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
[Symbol.asyncIterator]: function() {
|
|
61
|
+
return {
|
|
62
|
+
next: function() {
|
|
63
|
+
if (queue.length > 0) {
|
|
64
|
+
return Promise.resolve({ value: queue.shift(), done: false });
|
|
65
|
+
}
|
|
66
|
+
if (ended) {
|
|
67
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
68
|
+
}
|
|
69
|
+
return new Promise(function(resolve) {
|
|
70
|
+
waiting = resolve;
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --- SDK loader ---
|
|
79
|
+
function getSDK() {
|
|
80
|
+
if (!sdkModule) sdkModule = import("@anthropic-ai/claude-agent-sdk");
|
|
81
|
+
return sdkModule;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --- IPC helpers ---
|
|
85
|
+
function sendToDaemon(msg) {
|
|
86
|
+
if (!conn || conn.destroyed) return;
|
|
87
|
+
try {
|
|
88
|
+
conn.write(JSON.stringify(msg) + "\n");
|
|
89
|
+
} catch (e) {
|
|
90
|
+
console.error("[sdk-worker] Failed to send message:", e.message);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function handleMessage(msg) {
|
|
95
|
+
try { require("fs").writeSync(2, "[sdk-worker] MSG: " + msg.type + "\n"); } catch (e) {}
|
|
96
|
+
switch (msg.type) {
|
|
97
|
+
case "query_start":
|
|
98
|
+
handleQueryStart(msg);
|
|
99
|
+
break;
|
|
100
|
+
case "push_message":
|
|
101
|
+
handlePushMessage(msg);
|
|
102
|
+
break;
|
|
103
|
+
case "end_messages":
|
|
104
|
+
if (messageQueue) messageQueue.end();
|
|
105
|
+
break;
|
|
106
|
+
case "abort":
|
|
107
|
+
if (abortController) abortController.abort();
|
|
108
|
+
break;
|
|
109
|
+
case "set_model":
|
|
110
|
+
handleSetModel(msg);
|
|
111
|
+
break;
|
|
112
|
+
case "set_effort":
|
|
113
|
+
handleSetEffort(msg);
|
|
114
|
+
break;
|
|
115
|
+
case "set_permission_mode":
|
|
116
|
+
handleSetPermissionMode(msg);
|
|
117
|
+
break;
|
|
118
|
+
case "stop_task":
|
|
119
|
+
handleStopTask(msg);
|
|
120
|
+
break;
|
|
121
|
+
case "permission_response":
|
|
122
|
+
handlePermissionResponse(msg);
|
|
123
|
+
break;
|
|
124
|
+
case "ask_user_response":
|
|
125
|
+
handleAskUserResponse(msg);
|
|
126
|
+
break;
|
|
127
|
+
case "elicitation_response":
|
|
128
|
+
handleElicitationResponse(msg);
|
|
129
|
+
break;
|
|
130
|
+
case "warmup":
|
|
131
|
+
handleWarmup(msg);
|
|
132
|
+
break;
|
|
133
|
+
case "shutdown":
|
|
134
|
+
gracefulExit(0);
|
|
135
|
+
break;
|
|
136
|
+
default:
|
|
137
|
+
console.error("[sdk-worker] Unknown message type:", msg.type);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// --- canUseTool: delegates to daemon via IPC ---
|
|
142
|
+
function canUseTool(toolName, input, opts) {
|
|
143
|
+
var requestId = crypto.randomUUID();
|
|
144
|
+
sendToDaemon({
|
|
145
|
+
type: "permission_request",
|
|
146
|
+
requestId: requestId,
|
|
147
|
+
toolName: toolName,
|
|
148
|
+
input: input,
|
|
149
|
+
toolUseId: opts.toolUseID || "",
|
|
150
|
+
decisionReason: opts.decisionReason || "",
|
|
151
|
+
});
|
|
152
|
+
return new Promise(function(resolve) {
|
|
153
|
+
pendingPermissions[requestId] = resolve;
|
|
154
|
+
if (opts.signal) {
|
|
155
|
+
opts.signal.addEventListener("abort", function() {
|
|
156
|
+
delete pendingPermissions[requestId];
|
|
157
|
+
resolve({ behavior: "deny", message: "Cancelled" });
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// --- onElicitation: delegates to daemon via IPC ---
|
|
164
|
+
function onElicitation(request, opts) {
|
|
165
|
+
var requestId = crypto.randomUUID();
|
|
166
|
+
sendToDaemon({
|
|
167
|
+
type: "elicitation_request",
|
|
168
|
+
requestId: requestId,
|
|
169
|
+
serverName: request.serverName,
|
|
170
|
+
message: request.message,
|
|
171
|
+
mode: request.mode || "form",
|
|
172
|
+
url: request.url || null,
|
|
173
|
+
elicitationId: request.elicitationId || null,
|
|
174
|
+
requestedSchema: request.requestedSchema || null,
|
|
175
|
+
});
|
|
176
|
+
return new Promise(function(resolve) {
|
|
177
|
+
pendingElicitations[requestId] = resolve;
|
|
178
|
+
if (opts.signal) {
|
|
179
|
+
opts.signal.addEventListener("abort", function() {
|
|
180
|
+
delete pendingElicitations[requestId];
|
|
181
|
+
resolve({ action: "reject" });
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function handlePermissionResponse(msg) {
|
|
188
|
+
var resolve = pendingPermissions[msg.requestId];
|
|
189
|
+
if (resolve) {
|
|
190
|
+
delete pendingPermissions[msg.requestId];
|
|
191
|
+
resolve(msg.result);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function handleAskUserResponse(msg) {
|
|
196
|
+
var resolve = pendingAskUser[msg.toolUseId];
|
|
197
|
+
if (resolve) {
|
|
198
|
+
delete pendingAskUser[msg.toolUseId];
|
|
199
|
+
resolve(msg.result);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function handleElicitationResponse(msg) {
|
|
204
|
+
var resolve = pendingElicitations[msg.requestId];
|
|
205
|
+
if (resolve) {
|
|
206
|
+
delete pendingElicitations[msg.requestId];
|
|
207
|
+
resolve(msg.result);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// --- Query handling ---
|
|
212
|
+
async function handleQueryStart(msg) {
|
|
213
|
+
var t0 = msg._perfT0 || Date.now();
|
|
214
|
+
var localT0 = Date.now();
|
|
215
|
+
function perf(label) { console.log("[PERF] sdk-worker: " + label + " +" + (Date.now() - t0) + "ms (local +" + (Date.now() - localT0) + "ms)"); }
|
|
216
|
+
perf("handleQueryStart entered");
|
|
217
|
+
|
|
218
|
+
var sdk;
|
|
219
|
+
try {
|
|
220
|
+
perf("loading SDK");
|
|
221
|
+
sdk = await getSDK();
|
|
222
|
+
perf("SDK loaded");
|
|
223
|
+
} catch (e) {
|
|
224
|
+
sendToDaemon({ type: "query_error", error: "Failed to load SDK: " + (e.message || e), exitCode: null, stderr: null });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
messageQueue = createMessageQueue();
|
|
229
|
+
abortController = new AbortController();
|
|
230
|
+
|
|
231
|
+
// Push the initial user message
|
|
232
|
+
if (msg.prompt) {
|
|
233
|
+
messageQueue.push(msg.prompt);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Build query options (callbacks are local, everything else from daemon)
|
|
237
|
+
var options = msg.options || {};
|
|
238
|
+
options.abortController = abortController;
|
|
239
|
+
options.debug = true;
|
|
240
|
+
options.debugFile = "/tmp/clay-cli-debug-" + process.pid + ".log";
|
|
241
|
+
// Override CLI subprocess spawn to inject NODE_OPTIONS for IPv4-first DNS.
|
|
242
|
+
// The SDK constructs its own env for the CLI process, so worker env vars
|
|
243
|
+
// like NODE_OPTIONS are not inherited. We intercept the spawn to fix this.
|
|
244
|
+
options.spawnClaudeCodeProcess = function(spawnOpts) {
|
|
245
|
+
// Force IPv4-only at every level: preload script patches dns.lookup to
|
|
246
|
+
// only return IPv4, disables autoSelectFamily, and sets ipv4first order.
|
|
247
|
+
// This is needed because the CLI's Axios-based HTTP client ignores
|
|
248
|
+
// NODE_OPTIONS dns flags and still attempts IPv6 connections via its
|
|
249
|
+
// custom TLS agent, causing 5-10s timeouts on IPv6-less servers.
|
|
250
|
+
var preloadScript = require("path").join(__dirname, "ipv4-only.js");
|
|
251
|
+
var extraOpts = " --require " + JSON.stringify(preloadScript);
|
|
252
|
+
extraOpts += " --dns-result-order=ipv4first --no-network-family-autoselection";
|
|
253
|
+
spawnOpts.env.NODE_OPTIONS = (spawnOpts.env.NODE_OPTIONS || "") + extraOpts;
|
|
254
|
+
console.log("[sdk-worker] spawnClaudeCodeProcess called, command=" + spawnOpts.command);
|
|
255
|
+
var cp = require("child_process").spawn(spawnOpts.command, spawnOpts.args, {
|
|
256
|
+
cwd: spawnOpts.cwd,
|
|
257
|
+
env: spawnOpts.env,
|
|
258
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
259
|
+
});
|
|
260
|
+
// Capture ALL CLI stderr
|
|
261
|
+
if (cp.stderr) {
|
|
262
|
+
cp.stderr.on("data", function(chunk) {
|
|
263
|
+
var lines = chunk.toString().split("\n");
|
|
264
|
+
for (var li = 0; li < lines.length; li++) {
|
|
265
|
+
var line = lines[li].trim();
|
|
266
|
+
if (line) console.log("[CLI-STDERR] " + line.substring(0, 500));
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return cp;
|
|
271
|
+
};
|
|
272
|
+
options.canUseTool = function(toolName, input, toolOpts) {
|
|
273
|
+
// AskUserQuestion is handled specially: we send it as a separate IPC type
|
|
274
|
+
// so the daemon can use its own AskUserQuestion handling logic
|
|
275
|
+
if (toolName === "AskUserQuestion") {
|
|
276
|
+
var toolUseId = toolOpts.toolUseID || "";
|
|
277
|
+
sendToDaemon({
|
|
278
|
+
type: "ask_user_request",
|
|
279
|
+
toolUseId: toolUseId,
|
|
280
|
+
input: input,
|
|
281
|
+
});
|
|
282
|
+
return new Promise(function(resolve) {
|
|
283
|
+
pendingAskUser[toolUseId] = resolve;
|
|
284
|
+
if (toolOpts.signal) {
|
|
285
|
+
toolOpts.signal.addEventListener("abort", function() {
|
|
286
|
+
delete pendingAskUser[toolUseId];
|
|
287
|
+
resolve({ behavior: "deny", message: "Cancelled" });
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return canUseTool(toolName, input, toolOpts);
|
|
293
|
+
};
|
|
294
|
+
options.onElicitation = function(request, elicitOpts) {
|
|
295
|
+
return onElicitation(request, elicitOpts);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
perf("creating query instance");
|
|
299
|
+
try {
|
|
300
|
+
queryInstance = sdk.query({
|
|
301
|
+
prompt: messageQueue,
|
|
302
|
+
options: options,
|
|
303
|
+
});
|
|
304
|
+
perf("query instance created");
|
|
305
|
+
} catch (e) {
|
|
306
|
+
sendToDaemon({ type: "query_error", error: "Failed to create query: " + (e.message || e), exitCode: null, stderr: null });
|
|
307
|
+
queryInstance = null;
|
|
308
|
+
messageQueue = null;
|
|
309
|
+
abortController = null;
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// If single-turn, end the message queue immediately
|
|
314
|
+
if (msg.singleTurn) {
|
|
315
|
+
messageQueue.end();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Stream events to daemon
|
|
319
|
+
try {
|
|
320
|
+
var firstEvent = true;
|
|
321
|
+
var firstText = true;
|
|
322
|
+
var eventCounts = {};
|
|
323
|
+
for await (var event of queryInstance) {
|
|
324
|
+
var etype = (event && event.type || "?");
|
|
325
|
+
var esubtype = (event && event.subtype || "");
|
|
326
|
+
eventCounts[etype] = (eventCounts[etype] || 0) + 1;
|
|
327
|
+
if (firstEvent) {
|
|
328
|
+
perf("FIRST event from SDK (type=" + etype + " subtype=" + esubtype + ")");
|
|
329
|
+
firstEvent = false;
|
|
330
|
+
}
|
|
331
|
+
// Log every non-content event, and the first content/text event
|
|
332
|
+
if (etype !== "content_block_delta" && etype !== "content_block_start" && etype !== "content_block_stop") {
|
|
333
|
+
var extraInfo = "";
|
|
334
|
+
if (esubtype === "api_retry") {
|
|
335
|
+
// Dump full event to see all available fields
|
|
336
|
+
try {
|
|
337
|
+
var retryDump = JSON.stringify(event, function(k, v) {
|
|
338
|
+
if (typeof v === "string" && v.length > 200) return v.substring(0, 200) + "...[truncated]";
|
|
339
|
+
return v;
|
|
340
|
+
});
|
|
341
|
+
extraInfo = " FULL=" + retryDump;
|
|
342
|
+
} catch (je) {
|
|
343
|
+
extraInfo = " keys=" + Object.keys(event).join(",");
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
perf("SDK event #" + eventCounts[etype] + " type=" + etype + " subtype=" + esubtype + extraInfo);
|
|
347
|
+
}
|
|
348
|
+
if (firstText && (etype === "content_block_delta" || etype === "assistant" || (etype === "content_block_start"))) {
|
|
349
|
+
perf("FIRST TEXT/CONTENT event (type=" + etype + " subtype=" + esubtype + ")");
|
|
350
|
+
firstText = false;
|
|
351
|
+
}
|
|
352
|
+
sendToDaemon({ type: "sdk_event", event: event });
|
|
353
|
+
}
|
|
354
|
+
perf("all events streamed (counts=" + JSON.stringify(eventCounts) + "), fetching context usage");
|
|
355
|
+
// Fetch context usage breakdown before queryInstance is cleared
|
|
356
|
+
try {
|
|
357
|
+
if (queryInstance && typeof queryInstance.getContextUsage === "function") {
|
|
358
|
+
var ctxUsage = await queryInstance.getContextUsage();
|
|
359
|
+
sendToDaemon({ type: "context_usage", data: ctxUsage });
|
|
360
|
+
perf("context usage sent");
|
|
361
|
+
}
|
|
362
|
+
} catch (e) {
|
|
363
|
+
// Non-fatal: SDK may have already shut down
|
|
364
|
+
console.error("[sdk-worker] getContextUsage failed (non-fatal):", e.message);
|
|
365
|
+
}
|
|
366
|
+
perf("sending query_done");
|
|
367
|
+
sendToDaemon({ type: "query_done" });
|
|
368
|
+
} catch (err) {
|
|
369
|
+
var errMsg = err.message || String(err);
|
|
370
|
+
sendToDaemon({
|
|
371
|
+
type: "query_error",
|
|
372
|
+
error: errMsg,
|
|
373
|
+
exitCode: err.exitCode != null ? err.exitCode : null,
|
|
374
|
+
stderr: err.stderr || null,
|
|
375
|
+
});
|
|
376
|
+
} finally {
|
|
377
|
+
queryInstance = null;
|
|
378
|
+
messageQueue = null;
|
|
379
|
+
abortController = null;
|
|
380
|
+
pendingPermissions = {};
|
|
381
|
+
pendingAskUser = {};
|
|
382
|
+
pendingElicitations = {};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function handlePushMessage(msg) {
|
|
387
|
+
if (!messageQueue) return;
|
|
388
|
+
messageQueue.push(msg.content);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function handleSetModel(msg) {
|
|
392
|
+
if (!queryInstance) return;
|
|
393
|
+
try {
|
|
394
|
+
await queryInstance.setModel(msg.model);
|
|
395
|
+
sendToDaemon({ type: "model_changed", model: msg.model });
|
|
396
|
+
} catch (e) {
|
|
397
|
+
sendToDaemon({ type: "worker_error", error: "Failed to set model: " + (e.message || e) });
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function handleSetEffort(msg) {
|
|
402
|
+
if (!queryInstance) return;
|
|
403
|
+
try {
|
|
404
|
+
await queryInstance.setEffort(msg.effort);
|
|
405
|
+
sendToDaemon({ type: "effort_changed", effort: msg.effort });
|
|
406
|
+
} catch (e) {
|
|
407
|
+
sendToDaemon({ type: "worker_error", error: "Failed to set effort: " + (e.message || e) });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function handleSetPermissionMode(msg) {
|
|
412
|
+
if (!queryInstance) return;
|
|
413
|
+
try {
|
|
414
|
+
await queryInstance.setPermissionMode(msg.mode);
|
|
415
|
+
sendToDaemon({ type: "permission_mode_changed", mode: msg.mode });
|
|
416
|
+
} catch (e) {
|
|
417
|
+
sendToDaemon({ type: "worker_error", error: "Failed to set permission mode: " + (e.message || e) });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function handleStopTask(msg) {
|
|
422
|
+
if (!queryInstance) return;
|
|
423
|
+
try {
|
|
424
|
+
await queryInstance.stopTask(msg.taskId);
|
|
425
|
+
} catch (e) {
|
|
426
|
+
console.error("[sdk-worker] stopTask error:", e.message);
|
|
427
|
+
}
|
|
428
|
+
// Also abort as fallback (matches daemon behavior)
|
|
429
|
+
if (abortController) {
|
|
430
|
+
abortController.abort();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// --- Warmup ---
|
|
435
|
+
async function handleWarmup(msg) {
|
|
436
|
+
var sdk;
|
|
437
|
+
try {
|
|
438
|
+
sdk = await getSDK();
|
|
439
|
+
} catch (e) {
|
|
440
|
+
sendToDaemon({ type: "warmup_error", error: "Failed to load SDK: " + (e.message || e) });
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
var ac = new AbortController();
|
|
445
|
+
var mq = createMessageQueue();
|
|
446
|
+
mq.push({ type: "user", message: { role: "user", content: [{ type: "text", text: "hi" }] } });
|
|
447
|
+
mq.end();
|
|
448
|
+
|
|
449
|
+
var warmupOptions = msg.options || {};
|
|
450
|
+
warmupOptions.abortController = ac;
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
var stream = sdk.query({
|
|
454
|
+
prompt: mq,
|
|
455
|
+
options: warmupOptions,
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
for await (var event of stream) {
|
|
459
|
+
if (event.type === "system" && event.subtype === "init") {
|
|
460
|
+
var result = {
|
|
461
|
+
slashCommands: event.slash_commands || [],
|
|
462
|
+
model: event.model || "",
|
|
463
|
+
skills: event.skills || [],
|
|
464
|
+
fastModeState: event.fast_mode_state || null,
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// Fetch available models before aborting
|
|
468
|
+
try {
|
|
469
|
+
var models = await stream.supportedModels();
|
|
470
|
+
result.models = models || [];
|
|
471
|
+
} catch (e) {
|
|
472
|
+
result.models = [];
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
sendToDaemon({ type: "warmup_done", result: result });
|
|
476
|
+
ac.abort();
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
} catch (e) {
|
|
481
|
+
if (e && e.name !== "AbortError" && !(e.message && e.message.indexOf("aborted") !== -1)) {
|
|
482
|
+
sendToDaemon({ type: "warmup_error", error: "Warmup failed: " + (e.message || e) });
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// --- Cleanup ---
|
|
488
|
+
var _exitScheduled = false;
|
|
489
|
+
function cleanup() {
|
|
490
|
+
if (_keepAlive) {
|
|
491
|
+
try { clearInterval(_keepAlive); } catch (e) {}
|
|
492
|
+
}
|
|
493
|
+
if (abortController) {
|
|
494
|
+
try { abortController.abort(); } catch (e) {}
|
|
495
|
+
}
|
|
496
|
+
if (messageQueue) {
|
|
497
|
+
try { messageQueue.end(); } catch (e) {}
|
|
498
|
+
}
|
|
499
|
+
if (conn && !conn.destroyed) {
|
|
500
|
+
try { conn.end(); } catch (e) {}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Exit with a grace period so the SDK can flush session state to disk.
|
|
505
|
+
// Without this, process.exit(0) kills pending async writes and the
|
|
506
|
+
// session file may be incomplete, causing "no conversation found" on resume.
|
|
507
|
+
function gracefulExit(code) {
|
|
508
|
+
if (_exitScheduled) return;
|
|
509
|
+
_exitScheduled = true;
|
|
510
|
+
cleanup();
|
|
511
|
+
setTimeout(function() { process.exit(code); }, 800);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Keep event loop alive — without this, Node may exit if the socket handle
|
|
515
|
+
// gets unreferenced (observed on Linux with uid/gid spawn)
|
|
516
|
+
var _keepAlive = setInterval(function() {}, 30000);
|
|
517
|
+
|
|
518
|
+
// --- Connect to daemon socket ---
|
|
519
|
+
try { require("fs").writeSync(2, "[sdk-worker] Connecting to socket: " + socketPath + " +" + (Date.now() - _workerBootTs) + "ms since boot\n"); } catch (e) {}
|
|
520
|
+
conn = net.connect(socketPath, function() {
|
|
521
|
+
try { require("fs").writeSync(2, "[sdk-worker] Connected, sending ready +" + (Date.now() - _workerBootTs) + "ms since boot\n"); } catch (e) {}
|
|
522
|
+
sendToDaemon({ type: "ready" });
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
conn.on("data", function(chunk) {
|
|
526
|
+
buffer += chunk.toString();
|
|
527
|
+
var lines = buffer.split("\n");
|
|
528
|
+
buffer = lines.pop(); // keep incomplete line in buffer
|
|
529
|
+
for (var i = 0; i < lines.length; i++) {
|
|
530
|
+
if (!lines[i].trim()) continue;
|
|
531
|
+
try {
|
|
532
|
+
var msg = JSON.parse(lines[i]);
|
|
533
|
+
handleMessage(msg);
|
|
534
|
+
} catch (e) {
|
|
535
|
+
console.error("[sdk-worker] Failed to parse message:", e.message);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
conn.on("error", function(err) {
|
|
541
|
+
console.error("[sdk-worker] Socket error:", err.message);
|
|
542
|
+
gracefulExit(1);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
conn.on("close", function() {
|
|
546
|
+
try { require("fs").writeSync(2, "[sdk-worker] EXIT REASON: socket closed\n"); } catch (e) {}
|
|
547
|
+
gracefulExit(0);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// Handle process signals
|
|
551
|
+
process.on("SIGTERM", function() {
|
|
552
|
+
try { require("fs").writeSync(2, "[sdk-worker] EXIT REASON: SIGTERM\n"); } catch (e) {}
|
|
553
|
+
gracefulExit(0);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
process.on("SIGINT", function() {
|
|
557
|
+
try { require("fs").writeSync(2, "[sdk-worker] EXIT REASON: SIGINT\n"); } catch (e) {}
|
|
558
|
+
gracefulExit(0);
|
|
559
|
+
});
|