clay-server 2.23.1 → 2.24.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/build-user-env.js +6 -0
- package/lib/daemon.js +13 -0
- package/lib/ipv4-only.js +39 -0
- package/lib/project.js +333 -42
- package/lib/public/app.js +119 -69
- package/lib/public/claude-code-avatar.png +0 -0
- package/lib/public/css/debate.css +35 -1
- package/lib/public/css/filebrowser.css +2 -1
- package/lib/public/css/icon-strip.css +23 -0
- package/lib/public/css/input.css +66 -0
- package/lib/public/css/loop.css +0 -2
- package/lib/public/css/mates.css +113 -6
- package/lib/public/css/mention.css +26 -1
- package/lib/public/css/messages.css +97 -0
- package/lib/public/css/overlays.css +0 -4
- package/lib/public/css/server-settings.css +53 -0
- package/lib/public/css/session-search.css +1 -1
- package/lib/public/css/sidebar.css +26 -2
- package/lib/public/index.html +53 -13
- package/lib/public/modules/debate.js +158 -1
- package/lib/public/modules/filebrowser.js +11 -0
- package/lib/public/modules/input.js +20 -2
- package/lib/public/modules/markdown.js +2 -2
- package/lib/public/modules/mention.js +82 -32
- package/lib/public/modules/notifications.js +5 -1
- package/lib/public/modules/session-search.js +5 -5
- package/lib/public/modules/sidebar.js +39 -26
- package/lib/public/modules/theme.js +30 -0
- package/lib/public/modules/user-settings.js +61 -12
- package/lib/sdk-bridge.js +83 -78
- package/lib/sdk-worker.js +83 -3
- package/lib/server.js +93 -3
- package/lib/session-search.js +40 -5
- package/lib/sessions.js +2 -2
- package/lib/users.js +38 -0
- package/package.json +1 -1
package/lib/sdk-bridge.js
CHANGED
|
@@ -189,6 +189,26 @@ function createSDKBridge(opts) {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
function processSDKMessage(session, parsed) {
|
|
192
|
+
// Timing: log key SDK milestones relative to query start
|
|
193
|
+
if (session._queryStartTs) {
|
|
194
|
+
var _elapsed = Date.now() - session._queryStartTs;
|
|
195
|
+
if (parsed.type === "system" && parsed.subtype === "init") {
|
|
196
|
+
console.log("[PERF] processSDKMessage: system/init +" + _elapsed + "ms");
|
|
197
|
+
}
|
|
198
|
+
if (parsed.type === "stream_event" && parsed.event) {
|
|
199
|
+
if (parsed.event.type === "message_start") {
|
|
200
|
+
console.log("[PERF] processSDKMessage: message_start (API response begun) +" + _elapsed + "ms");
|
|
201
|
+
}
|
|
202
|
+
if (parsed.event.type === "content_block_delta" && !session._firstTextLogged) {
|
|
203
|
+
session._firstTextLogged = true;
|
|
204
|
+
console.log("[PERF] processSDKMessage: FIRST content_block_delta (visible text) +" + _elapsed + "ms");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (parsed.type === "result") {
|
|
208
|
+
console.log("[PERF] processSDKMessage: result +" + _elapsed + "ms");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
192
212
|
// Extract session_id from any message that carries it
|
|
193
213
|
if (parsed.session_id && !session.cliSessionId) {
|
|
194
214
|
session.cliSessionId = parsed.session_id;
|
|
@@ -756,7 +776,9 @@ function createSDKBridge(opts) {
|
|
|
756
776
|
});
|
|
757
777
|
|
|
758
778
|
// Create Unix socket server
|
|
779
|
+
var spawnT0 = Date.now();
|
|
759
780
|
worker.server = net.createServer(function(connection) {
|
|
781
|
+
console.log("[PERF] spawnWorker: socket connection accepted +" + (Date.now() - spawnT0) + "ms");
|
|
760
782
|
worker.connection = connection;
|
|
761
783
|
connection.on("data", function(chunk) {
|
|
762
784
|
worker.buffer += chunk.toString();
|
|
@@ -767,6 +789,7 @@ function createSDKBridge(opts) {
|
|
|
767
789
|
try {
|
|
768
790
|
var msg = JSON.parse(lines[i]);
|
|
769
791
|
if (msg.type === "ready") {
|
|
792
|
+
console.log("[PERF] spawnWorker: 'ready' IPC received +" + (Date.now() - spawnT0) + "ms");
|
|
770
793
|
worker.ready = true;
|
|
771
794
|
if (worker._readyResolve) {
|
|
772
795
|
worker._readyResolve();
|
|
@@ -787,6 +810,7 @@ function createSDKBridge(opts) {
|
|
|
787
810
|
});
|
|
788
811
|
|
|
789
812
|
worker.server.listen(socketPath, function() {
|
|
813
|
+
console.log("[PERF] spawnWorker: socket listen ready +" + (Date.now() - spawnT0) + "ms");
|
|
790
814
|
// Set socket permissions so the target user can connect
|
|
791
815
|
try { fs.chmodSync(socketPath, 0o777); } catch (e) {}
|
|
792
816
|
|
|
@@ -933,125 +957,96 @@ function createSDKBridge(opts) {
|
|
|
933
957
|
* Mirrors the in-process startQuery flow but delegates SDK execution to the worker.
|
|
934
958
|
*/
|
|
935
959
|
async function startQueryViaWorker(session, text, images, linuxUser) {
|
|
960
|
+
var t0 = session._queryStartTs || Date.now();
|
|
961
|
+
function perf(label) { console.log("[PERF] sdk-bridge: " + label + " +" + (Date.now() - t0) + "ms"); }
|
|
962
|
+
perf("startQueryViaWorker entered");
|
|
963
|
+
|
|
936
964
|
// Wait for the previous worker to fully exit before spawning a new one.
|
|
937
965
|
// Without this, the new worker may try to resume the SDK session file
|
|
938
966
|
// while the old worker is still flushing it to disk (800ms grace period),
|
|
939
967
|
// causing "no conversation found" and losing all prior context.
|
|
940
968
|
if (session._workerExitPromise) {
|
|
941
|
-
|
|
969
|
+
perf("waiting for old worker exit");
|
|
942
970
|
var exitWait = session._workerExitPromise;
|
|
943
971
|
session._workerExitPromise = null;
|
|
944
972
|
await Promise.race([
|
|
945
973
|
exitWait,
|
|
946
974
|
new Promise(function(resolve) { setTimeout(resolve, 3000); }),
|
|
947
975
|
]);
|
|
948
|
-
|
|
976
|
+
perf("old worker exit wait done");
|
|
949
977
|
}
|
|
950
978
|
|
|
951
|
-
//
|
|
952
|
-
//
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
if (
|
|
979
|
+
// Reuse existing worker if alive, otherwise spawn a new one.
|
|
980
|
+
// Spawn FIRST so the worker starts booting while we do dir setup below.
|
|
981
|
+
var worker;
|
|
982
|
+
var reusingWorker = false;
|
|
983
|
+
if (session.worker && session.worker.ready && session.worker.process && !session.worker.process.killed) {
|
|
984
|
+
worker = session.worker;
|
|
985
|
+
reusingWorker = true;
|
|
986
|
+
// Clear old message handlers so they don't fire for the new query
|
|
987
|
+
worker.messageHandlers = [];
|
|
988
|
+
worker._queryEnded = false;
|
|
989
|
+
worker._abortSent = false;
|
|
990
|
+
perf("reusing existing worker pid=" + (worker.process ? worker.process.pid : "?"));
|
|
991
|
+
} else {
|
|
956
992
|
try {
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
var claudeDir = path.join(linuxUserHome0, ".claude");
|
|
962
|
-
var projectSlug0 = (cwd || "").replace(/\//g, "-");
|
|
963
|
-
var projDir = path.join(claudeDir, "projects", projectSlug0);
|
|
964
|
-
// Create the project directory if missing and chown the whole tree
|
|
965
|
-
if (!fs.existsSync(projDir)) {
|
|
966
|
-
fs.mkdirSync(projDir, { recursive: true });
|
|
967
|
-
try { require("child_process").execSync("chown -R " + uid0 + " " + JSON.stringify(claudeDir)); } catch (e) {}
|
|
968
|
-
} else {
|
|
969
|
-
// Fix existing directory if root-owned
|
|
970
|
-
try {
|
|
971
|
-
var pstat = fs.statSync(projDir);
|
|
972
|
-
if (pstat.uid !== uid0) {
|
|
973
|
-
require("child_process").execSync("chown " + uid0 + " " + JSON.stringify(projDir));
|
|
974
|
-
}
|
|
975
|
-
} catch (e) {}
|
|
976
|
-
}
|
|
977
|
-
}
|
|
993
|
+
perf("spawning new worker");
|
|
994
|
+
worker = spawnWorker(linuxUser);
|
|
995
|
+
perf("spawnWorker returned");
|
|
996
|
+
session.worker = worker;
|
|
978
997
|
} catch (e) {
|
|
979
|
-
|
|
998
|
+
session.isProcessing = false;
|
|
999
|
+
onProcessingChanged();
|
|
1000
|
+
sendAndRecord(session, { type: "error", text: "Failed to spawn worker for " + linuxUser + ": " + (e.message || e) });
|
|
1001
|
+
sendAndRecord(session, { type: "done", code: 1 });
|
|
1002
|
+
sm.broadcastSessionList();
|
|
1003
|
+
return;
|
|
980
1004
|
}
|
|
981
1005
|
}
|
|
982
1006
|
|
|
983
|
-
//
|
|
984
|
-
//
|
|
985
|
-
//
|
|
986
|
-
|
|
1007
|
+
// Ensure the linux user's .claude project directory exists and is writable,
|
|
1008
|
+
// then pre-copy CLI session file if needed. This runs while the worker is
|
|
1009
|
+
// booting (readyPromise pending), so it adds no extra latency.
|
|
1010
|
+
perf("dir setup start");
|
|
1011
|
+
if (linuxUser) {
|
|
987
1012
|
try {
|
|
988
1013
|
var configMod = require("./config");
|
|
989
1014
|
var osUsersMod = require("./os-users");
|
|
990
1015
|
var originalHome = configMod.REAL_HOME || require("os").homedir();
|
|
991
1016
|
var linuxUserHome = osUsersMod.getLinuxUserHome(linuxUser);
|
|
992
|
-
|
|
1017
|
+
var uid = osUsersMod.getLinuxUserUid(linuxUser);
|
|
1018
|
+
if (originalHome !== linuxUserHome && uid != null) {
|
|
993
1019
|
var projectSlug = (cwd || "").replace(/\//g, "-");
|
|
994
|
-
var sessionFileName = session.cliSessionId + ".jsonl";
|
|
995
|
-
var srcFile = path.join(originalHome, ".claude", "projects", projectSlug, sessionFileName);
|
|
996
1020
|
var dstDir = path.join(linuxUserHome, ".claude", "projects", projectSlug);
|
|
997
|
-
|
|
998
|
-
var uid = osUsersMod.getLinuxUserUid(linuxUser);
|
|
999
|
-
// Ensure the projects directory is owned by the linux user so the
|
|
1000
|
-
// SDK can create new session files. Without this, mkdirSync creates
|
|
1001
|
-
// root-owned directories and the SDK silently fails to save sessions.
|
|
1021
|
+
// Create and chown the project directory once
|
|
1002
1022
|
if (!fs.existsSync(dstDir)) {
|
|
1003
1023
|
fs.mkdirSync(dstDir, { recursive: true });
|
|
1004
|
-
|
|
1005
|
-
try { require("child_process").execSync("chown -R " + uid + " " + JSON.stringify(dstDir)); } catch (e2) {}
|
|
1006
|
-
}
|
|
1024
|
+
try { require("child_process").execSync("chown -R " + uid + " " + JSON.stringify(path.join(linuxUserHome, ".claude"))); } catch (e2) {}
|
|
1007
1025
|
} else {
|
|
1008
|
-
// Fix ownership of existing directories created by root
|
|
1009
1026
|
try {
|
|
1010
1027
|
var dirStat = fs.statSync(dstDir);
|
|
1011
|
-
if (
|
|
1028
|
+
if (dirStat.uid !== uid) {
|
|
1012
1029
|
require("child_process").execSync("chown " + uid + " " + JSON.stringify(dstDir));
|
|
1013
1030
|
}
|
|
1014
1031
|
} catch (e2) {}
|
|
1015
1032
|
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1033
|
+
// Pre-copy CLI session file so the worker can resume the conversation
|
|
1034
|
+
if (session.cliSessionId) {
|
|
1035
|
+
var sessionFileName = session.cliSessionId + ".jsonl";
|
|
1036
|
+
var srcFile = path.join(originalHome, ".claude", "projects", projectSlug, sessionFileName);
|
|
1037
|
+
var dstFile = path.join(dstDir, sessionFileName);
|
|
1038
|
+
if (fs.existsSync(srcFile) && !fs.existsSync(dstFile)) {
|
|
1039
|
+
fs.copyFileSync(srcFile, dstFile);
|
|
1019
1040
|
try { require("child_process").execSync("chown " + uid + " " + JSON.stringify(dstFile)); } catch (e2) {}
|
|
1041
|
+
console.log("[sdk-bridge] Pre-copied CLI session " + session.cliSessionId + " to " + linuxUser);
|
|
1020
1042
|
}
|
|
1021
|
-
console.log("[sdk-bridge] Pre-copied CLI session " + session.cliSessionId + " to " + linuxUser);
|
|
1022
1043
|
}
|
|
1023
1044
|
}
|
|
1024
1045
|
} catch (copyErr) {
|
|
1025
|
-
console.log("[sdk-bridge]
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
// Reuse existing worker if alive, otherwise spawn a new one.
|
|
1030
|
-
// Keeping the worker alive between queries lets the SDK maintain session
|
|
1031
|
-
// state in memory, avoiding disk-based resume that fails on abort.
|
|
1032
|
-
var worker;
|
|
1033
|
-
var reusingWorker = false;
|
|
1034
|
-
if (session.worker && session.worker.ready && session.worker.process && !session.worker.process.killed) {
|
|
1035
|
-
worker = session.worker;
|
|
1036
|
-
reusingWorker = true;
|
|
1037
|
-
// Clear old message handlers so they don't fire for the new query
|
|
1038
|
-
worker.messageHandlers = [];
|
|
1039
|
-
worker._queryEnded = false;
|
|
1040
|
-
worker._abortSent = false;
|
|
1041
|
-
console.log("[sdk-bridge] Reusing existing worker pid=" + (worker.process ? worker.process.pid : "?"));
|
|
1042
|
-
} else {
|
|
1043
|
-
try {
|
|
1044
|
-
worker = spawnWorker(linuxUser);
|
|
1045
|
-
session.worker = worker;
|
|
1046
|
-
} catch (e) {
|
|
1047
|
-
session.isProcessing = false;
|
|
1048
|
-
onProcessingChanged();
|
|
1049
|
-
sendAndRecord(session, { type: "error", text: "Failed to spawn worker for " + linuxUser + ": " + (e.message || e) });
|
|
1050
|
-
sendAndRecord(session, { type: "done", code: 1 });
|
|
1051
|
-
sm.broadcastSessionList();
|
|
1052
|
-
return;
|
|
1046
|
+
console.log("[sdk-bridge] Dir setup / session pre-copy skipped:", copyErr.message);
|
|
1053
1047
|
}
|
|
1054
1048
|
}
|
|
1049
|
+
perf("dir setup done");
|
|
1055
1050
|
|
|
1056
1051
|
session.messageQueue = "worker"; // sentinel: messages go via worker IPC
|
|
1057
1052
|
session.blocks = {};
|
|
@@ -1119,7 +1114,12 @@ function createSDKBridge(opts) {
|
|
|
1119
1114
|
}
|
|
1120
1115
|
|
|
1121
1116
|
// Set up message handler for worker events
|
|
1117
|
+
var firstEventLogged = false;
|
|
1122
1118
|
worker.onMessage(function(msg) {
|
|
1119
|
+
if (!firstEventLogged && msg.type === "sdk_event") {
|
|
1120
|
+
firstEventLogged = true;
|
|
1121
|
+
perf("FIRST sdk_event received (type=" + (msg.event && msg.event.type || "?") + ")");
|
|
1122
|
+
}
|
|
1123
1123
|
switch (msg.type) {
|
|
1124
1124
|
case "sdk_event":
|
|
1125
1125
|
processSDKMessage(session, msg.event);
|
|
@@ -1312,8 +1312,10 @@ function createSDKBridge(opts) {
|
|
|
1312
1312
|
|
|
1313
1313
|
// Wait for worker to be ready, then send query
|
|
1314
1314
|
if (!reusingWorker) {
|
|
1315
|
+
perf("awaiting readyPromise");
|
|
1315
1316
|
try {
|
|
1316
1317
|
await worker.readyPromise;
|
|
1318
|
+
perf("readyPromise resolved");
|
|
1317
1319
|
} catch (e) {
|
|
1318
1320
|
session.isProcessing = false;
|
|
1319
1321
|
onProcessingChanged();
|
|
@@ -1325,6 +1327,7 @@ function createSDKBridge(opts) {
|
|
|
1325
1327
|
}
|
|
1326
1328
|
}
|
|
1327
1329
|
|
|
1330
|
+
perf("sending query_start to worker");
|
|
1328
1331
|
worker.send({
|
|
1329
1332
|
type: "query_start",
|
|
1330
1333
|
prompt: initialMessage,
|
|
@@ -1332,7 +1335,9 @@ function createSDKBridge(opts) {
|
|
|
1332
1335
|
singleTurn: !!session.singleTurn,
|
|
1333
1336
|
originalHome: require("./config").REAL_HOME || null,
|
|
1334
1337
|
projectPath: session.cwd || null,
|
|
1338
|
+
_perfT0: t0,
|
|
1335
1339
|
});
|
|
1340
|
+
perf("query_start sent");
|
|
1336
1341
|
}
|
|
1337
1342
|
|
|
1338
1343
|
function cleanupSessionWorker(session, fromWorker) {
|
package/lib/sdk-worker.js
CHANGED
|
@@ -4,8 +4,14 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Usage: node sdk-worker.js <socket-path>
|
|
6
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
|
+
|
|
7
12
|
// Early diagnostic — writes directly to fd 2 to ensure output even if pipes close fast
|
|
8
|
-
|
|
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) {}
|
|
9
15
|
|
|
10
16
|
var net = require("net");
|
|
11
17
|
var crypto = require("crypto");
|
|
@@ -204,9 +210,16 @@ function handleElicitationResponse(msg) {
|
|
|
204
210
|
|
|
205
211
|
// --- Query handling ---
|
|
206
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
|
+
|
|
207
218
|
var sdk;
|
|
208
219
|
try {
|
|
220
|
+
perf("loading SDK");
|
|
209
221
|
sdk = await getSDK();
|
|
222
|
+
perf("SDK loaded");
|
|
210
223
|
} catch (e) {
|
|
211
224
|
sendToDaemon({ type: "query_error", error: "Failed to load SDK: " + (e.message || e), exitCode: null, stderr: null });
|
|
212
225
|
return;
|
|
@@ -223,6 +236,39 @@ async function handleQueryStart(msg) {
|
|
|
223
236
|
// Build query options (callbacks are local, everything else from daemon)
|
|
224
237
|
var options = msg.options || {};
|
|
225
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
|
+
};
|
|
226
272
|
options.canUseTool = function(toolName, input, toolOpts) {
|
|
227
273
|
// AskUserQuestion is handled specially: we send it as a separate IPC type
|
|
228
274
|
// so the daemon can use its own AskUserQuestion handling logic
|
|
@@ -249,11 +295,13 @@ async function handleQueryStart(msg) {
|
|
|
249
295
|
return onElicitation(request, elicitOpts);
|
|
250
296
|
};
|
|
251
297
|
|
|
298
|
+
perf("creating query instance");
|
|
252
299
|
try {
|
|
253
300
|
queryInstance = sdk.query({
|
|
254
301
|
prompt: messageQueue,
|
|
255
302
|
options: options,
|
|
256
303
|
});
|
|
304
|
+
perf("query instance created");
|
|
257
305
|
} catch (e) {
|
|
258
306
|
sendToDaemon({ type: "query_error", error: "Failed to create query: " + (e.message || e), exitCode: null, stderr: null });
|
|
259
307
|
queryInstance = null;
|
|
@@ -269,9 +317,41 @@ async function handleQueryStart(msg) {
|
|
|
269
317
|
|
|
270
318
|
// Stream events to daemon
|
|
271
319
|
try {
|
|
320
|
+
var firstEvent = true;
|
|
321
|
+
var firstText = true;
|
|
322
|
+
var eventCounts = {};
|
|
272
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
|
+
}
|
|
273
352
|
sendToDaemon({ type: "sdk_event", event: event });
|
|
274
353
|
}
|
|
354
|
+
perf("all events streamed (counts=" + JSON.stringify(eventCounts) + "), sending query_done");
|
|
275
355
|
sendToDaemon({ type: "query_done" });
|
|
276
356
|
} catch (err) {
|
|
277
357
|
var errMsg = err.message || String(err);
|
|
@@ -424,9 +504,9 @@ function gracefulExit(code) {
|
|
|
424
504
|
var _keepAlive = setInterval(function() {}, 30000);
|
|
425
505
|
|
|
426
506
|
// --- Connect to daemon socket ---
|
|
427
|
-
try { require("fs").writeSync(2, "[sdk-worker] Connecting to socket: " + socketPath + "\n"); } catch (e) {}
|
|
507
|
+
try { require("fs").writeSync(2, "[sdk-worker] Connecting to socket: " + socketPath + " +" + (Date.now() - _workerBootTs) + "ms since boot\n"); } catch (e) {}
|
|
428
508
|
conn = net.connect(socketPath, function() {
|
|
429
|
-
try { require("fs").writeSync(2, "[sdk-worker] Connected, sending ready\n"); } catch (e) {}
|
|
509
|
+
try { require("fs").writeSync(2, "[sdk-worker] Connected, sending ready +" + (Date.now() - _workerBootTs) + "ms since boot\n"); } catch (e) {}
|
|
430
510
|
sendToDaemon({ type: "ready" });
|
|
431
511
|
});
|
|
432
512
|
|
package/lib/server.js
CHANGED
|
@@ -1132,6 +1132,8 @@ function createServer(opts) {
|
|
|
1132
1132
|
profile.userId = mu.id;
|
|
1133
1133
|
profile.role = mu.role;
|
|
1134
1134
|
profile.autoContinueOnRateLimit = !!mu.autoContinueOnRateLimit;
|
|
1135
|
+
profile.chatLayout = mu.chatLayout || "channel";
|
|
1136
|
+
profile.mateOnboardingShown = !!mu.mateOnboardingShown;
|
|
1135
1137
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1136
1138
|
res.end(JSON.stringify(profile));
|
|
1137
1139
|
return;
|
|
@@ -1147,10 +1149,12 @@ function createServer(opts) {
|
|
|
1147
1149
|
if (saved.avatarSeed) profile.avatarSeed = saved.avatarSeed;
|
|
1148
1150
|
if (saved.avatarCustom) profile.avatarCustom = saved.avatarCustom;
|
|
1149
1151
|
} catch (e) { /* file doesn't exist yet */ }
|
|
1150
|
-
// Single-user
|
|
1152
|
+
// Single-user settings from daemon config
|
|
1151
1153
|
if (typeof opts.onGetDaemonConfig === "function") {
|
|
1152
1154
|
var dc = opts.onGetDaemonConfig();
|
|
1153
1155
|
profile.autoContinueOnRateLimit = !!dc.autoContinueOnRateLimit;
|
|
1156
|
+
profile.chatLayout = dc.chatLayout || "channel";
|
|
1157
|
+
profile.mateOnboardingShown = !!dc.mateOnboardingShown;
|
|
1154
1158
|
}
|
|
1155
1159
|
// Check if custom avatar file exists
|
|
1156
1160
|
try {
|
|
@@ -1262,7 +1266,10 @@ function createServer(opts) {
|
|
|
1262
1266
|
}
|
|
1263
1267
|
}
|
|
1264
1268
|
} catch (e) {}
|
|
1265
|
-
|
|
1269
|
+
var avatarFilePath = path.join(avatarDir, filename);
|
|
1270
|
+
fs.writeFileSync(avatarFilePath, raw);
|
|
1271
|
+
try { fs.chmodSync(avatarFilePath, 0o644); } catch (e) {}
|
|
1272
|
+
try { fs.chmodSync(avatarDir, 0o755); } catch (e) {}
|
|
1266
1273
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1267
1274
|
res.end(JSON.stringify({ ok: true, avatar: "/api/avatar/" + userId + "?v=" + Date.now() }));
|
|
1268
1275
|
});
|
|
@@ -1360,7 +1367,10 @@ function createServer(opts) {
|
|
|
1360
1367
|
}
|
|
1361
1368
|
}
|
|
1362
1369
|
} catch (e) {}
|
|
1363
|
-
|
|
1370
|
+
var mateAvatarFilePath = path.join(avatarDir, filename);
|
|
1371
|
+
fs.writeFileSync(mateAvatarFilePath, raw);
|
|
1372
|
+
try { fs.chmodSync(mateAvatarFilePath, 0o644); } catch (e) {}
|
|
1373
|
+
try { fs.chmodSync(avatarDir, 0o755); } catch (e) {}
|
|
1364
1374
|
var avatarPath = "/api/mate-avatar/" + mateIdFromUrl + "?v=" + Date.now();
|
|
1365
1375
|
// Update mate profile with custom avatar URL
|
|
1366
1376
|
var profile = mate.profile || {};
|
|
@@ -1483,6 +1493,68 @@ function createServer(opts) {
|
|
|
1483
1493
|
return;
|
|
1484
1494
|
}
|
|
1485
1495
|
|
|
1496
|
+
// PUT /api/user/chat-layout
|
|
1497
|
+
if (req.method === "PUT" && fullUrl === "/api/user/chat-layout") {
|
|
1498
|
+
var mu = getMultiUserFromReq(req);
|
|
1499
|
+
if (!mu) {
|
|
1500
|
+
// Single-user: save to daemon config
|
|
1501
|
+
var body = "";
|
|
1502
|
+
req.on("data", function (chunk) { body += chunk; });
|
|
1503
|
+
req.on("end", function () {
|
|
1504
|
+
try {
|
|
1505
|
+
var data = JSON.parse(body);
|
|
1506
|
+
var val = (data.layout === "bubble") ? "bubble" : "channel";
|
|
1507
|
+
if (typeof opts.onSetChatLayout === "function") {
|
|
1508
|
+
opts.onSetChatLayout(val);
|
|
1509
|
+
}
|
|
1510
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1511
|
+
res.end(JSON.stringify({ ok: true, chatLayout: val }));
|
|
1512
|
+
} catch (e) {
|
|
1513
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1514
|
+
res.end('{"error":"Invalid request"}');
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
var body = "";
|
|
1520
|
+
req.on("data", function (chunk) { body += chunk; });
|
|
1521
|
+
req.on("end", function () {
|
|
1522
|
+
try {
|
|
1523
|
+
var data = JSON.parse(body);
|
|
1524
|
+
var result = users.setChatLayout(mu.id, data.layout);
|
|
1525
|
+
if (result.error) {
|
|
1526
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1527
|
+
res.end(JSON.stringify({ error: result.error }));
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1531
|
+
res.end(JSON.stringify({ ok: true, chatLayout: result.chatLayout }));
|
|
1532
|
+
} catch (e) {
|
|
1533
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1534
|
+
res.end('{"error":"Invalid request"}');
|
|
1535
|
+
}
|
|
1536
|
+
});
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// POST /api/user/mate-onboarded
|
|
1541
|
+
if (req.method === "POST" && fullUrl === "/api/user/mate-onboarded") {
|
|
1542
|
+
var mu = getMultiUserFromReq(req);
|
|
1543
|
+
if (!mu) {
|
|
1544
|
+
// Single-user: save to daemon config
|
|
1545
|
+
if (typeof opts.onSetMateOnboarded === "function") {
|
|
1546
|
+
opts.onSetMateOnboarded();
|
|
1547
|
+
}
|
|
1548
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1549
|
+
res.end('{"ok":true}');
|
|
1550
|
+
} else {
|
|
1551
|
+
users.setMateOnboarded(mu.id);
|
|
1552
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1553
|
+
res.end('{"ok":true}');
|
|
1554
|
+
}
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1486
1558
|
// GET /api/user/auto-continue
|
|
1487
1559
|
if (req.method === "GET" && fullUrl === "/api/user/auto-continue") {
|
|
1488
1560
|
var mu = getMultiUserFromReq(req);
|
|
@@ -2829,6 +2901,24 @@ function createServer(opts) {
|
|
|
2829
2901
|
});
|
|
2830
2902
|
return list;
|
|
2831
2903
|
},
|
|
2904
|
+
getAllProjectSessions: function () {
|
|
2905
|
+
var allSessions = [];
|
|
2906
|
+
projects.forEach(function (pCtx, pSlug) {
|
|
2907
|
+
if (pSlug === slug) return; // skip self
|
|
2908
|
+
var status = pCtx.getStatus();
|
|
2909
|
+
if (status.isWorktree) return;
|
|
2910
|
+
var pSm = pCtx.getSessionManager();
|
|
2911
|
+
if (!pSm) return;
|
|
2912
|
+
var projectTitle = status.title || status.project || pSlug;
|
|
2913
|
+
pSm.sessions.forEach(function (s) {
|
|
2914
|
+
if (!s.hidden && s.history && s.history.length > 0) {
|
|
2915
|
+
s._projectTitle = projectTitle;
|
|
2916
|
+
allSessions.push(s);
|
|
2917
|
+
}
|
|
2918
|
+
});
|
|
2919
|
+
});
|
|
2920
|
+
return allSessions;
|
|
2921
|
+
},
|
|
2832
2922
|
getHubSchedules: function () {
|
|
2833
2923
|
var allSchedules = [];
|
|
2834
2924
|
projects.forEach(function (ctx, s) {
|
package/lib/session-search.js
CHANGED
|
@@ -412,12 +412,13 @@ function searchSessions(sessions, query, opts) {
|
|
|
412
412
|
}
|
|
413
413
|
|
|
414
414
|
/**
|
|
415
|
-
* Unified mate search: digests + session history combined.
|
|
416
|
-
* Returns merged, re-ranked results from
|
|
415
|
+
* Unified mate search: digests + session history + knowledge files combined.
|
|
416
|
+
* Returns merged, re-ranked results from all sources.
|
|
417
417
|
* For global search (e.g. Ally), pass otherDigests array with paths and mate names.
|
|
418
|
-
* @param {object} opts - { digestFilePath, otherDigests, sessions, query, maxResults, minScore }
|
|
418
|
+
* @param {object} opts - { digestFilePath, otherDigests, sessions, knowledgeFiles, query, maxResults, minScore }
|
|
419
419
|
* otherDigests: [{ path: string, mateName: string }] - other mates' digest files
|
|
420
|
-
*
|
|
420
|
+
* knowledgeFiles: [{ filePath: string, name: string, mateName: string }] - knowledge files to search
|
|
421
|
+
* @returns {Array} [{ source: "digest"|"session"|"knowledge", score, mateName, ... }]
|
|
421
422
|
*/
|
|
422
423
|
function searchMate(opts) {
|
|
423
424
|
if (!opts || !opts.query || !opts.query.trim()) return [];
|
|
@@ -447,9 +448,33 @@ function searchMate(opts) {
|
|
|
447
448
|
}
|
|
448
449
|
}
|
|
449
450
|
|
|
451
|
+
// Collect knowledge file docs
|
|
452
|
+
if (opts.knowledgeFiles && opts.knowledgeFiles.length > 0) {
|
|
453
|
+
for (var ki = 0; ki < opts.knowledgeFiles.length; ki++) {
|
|
454
|
+
var kf = opts.knowledgeFiles[ki];
|
|
455
|
+
try {
|
|
456
|
+
if (!fs.existsSync(kf.filePath)) continue;
|
|
457
|
+
var content = fs.readFileSync(kf.filePath, "utf8").trim();
|
|
458
|
+
if (!content || content.length < 20) continue;
|
|
459
|
+
// Cap at 4000 chars to avoid oversized docs dominating the index
|
|
460
|
+
var capped = content.length > 4000 ? content.substring(0, 4000) : content;
|
|
461
|
+
allDocs.push({
|
|
462
|
+
id: "knowledge:" + (kf.mateName || "self") + ":" + kf.name,
|
|
463
|
+
text: kf.name + " " + kf.name + " " + capped,
|
|
464
|
+
meta: {
|
|
465
|
+
source: "knowledge",
|
|
466
|
+
fileName: kf.name,
|
|
467
|
+
mateName: kf.mateName || null,
|
|
468
|
+
snippet: content.length > 200 ? content.substring(0, 200) + "..." : content
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
} catch (e) {}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
450
475
|
if (allDocs.length === 0) return [];
|
|
451
476
|
|
|
452
|
-
// Build unified index across
|
|
477
|
+
// Build unified index across all sources and search
|
|
453
478
|
var index = buildIndex(allDocs);
|
|
454
479
|
var results = searchIndex(index, opts.query, maxResults);
|
|
455
480
|
|
|
@@ -463,6 +488,10 @@ function searchMate(opts) {
|
|
|
463
488
|
out.digest = r.meta.digest;
|
|
464
489
|
out.lineIdx = r.meta.lineIdx;
|
|
465
490
|
out.mateName = r.meta.mateName || null;
|
|
491
|
+
} else if (r.meta.source === "knowledge") {
|
|
492
|
+
out.fileName = r.meta.fileName;
|
|
493
|
+
out.mateName = r.meta.mateName || null;
|
|
494
|
+
out.snippet = r.meta.snippet;
|
|
466
495
|
} else {
|
|
467
496
|
out.sessionId = r.meta.sessionId;
|
|
468
497
|
out.sessionTitle = r.meta.sessionTitle;
|
|
@@ -620,6 +649,12 @@ function formatForContext(results) {
|
|
|
620
649
|
}
|
|
621
650
|
line += " (relevance: " + scorePct + ")";
|
|
622
651
|
lines.push(line);
|
|
652
|
+
} else if (r.source === "knowledge") {
|
|
653
|
+
var line = "- [knowledge: " + (r.fileName || "?") + "] ";
|
|
654
|
+
if (r.mateName) line += "(@" + r.mateName + ") ";
|
|
655
|
+
line += (r.snippet || "(no preview)");
|
|
656
|
+
line += " (relevance: " + scorePct + ")";
|
|
657
|
+
lines.push(line);
|
|
623
658
|
} else if (r.source === "session") {
|
|
624
659
|
var line = "- [session: " + (r.sessionTitle || "?") + "] ";
|
|
625
660
|
line += (r.snippet || "(no preview)");
|
package/lib/sessions.js
CHANGED
|
@@ -577,7 +577,7 @@ function createSessionManager(opts) {
|
|
|
577
577
|
var contentMatch = false;
|
|
578
578
|
for (var i = 0; i < session.history.length; i++) {
|
|
579
579
|
var entry = session.history[i];
|
|
580
|
-
if ((entry.type === "delta" || entry.type === "user_message" || entry.type === "mention_user" || entry.type === "mention_response" || entry.type === "debate_turn_done") && entry.text) {
|
|
580
|
+
if ((entry.type === "delta" || entry.type === "user_message" || entry.type === "mention_user" || entry.type === "mention_response" || entry.type === "debate_turn_done" || entry.type === "debate_comment_injected") && entry.text) {
|
|
581
581
|
if (entry.text.toLowerCase().indexOf(q) !== -1) {
|
|
582
582
|
contentMatch = true;
|
|
583
583
|
break;
|
|
@@ -614,7 +614,7 @@ function createSessionManager(opts) {
|
|
|
614
614
|
currentTurnStart = i;
|
|
615
615
|
lastAssistantHitTurn = -1;
|
|
616
616
|
}
|
|
617
|
-
if ((entry.type === "delta" || entry.type === "user_message" || entry.type === "mention_user" || entry.type === "mention_response" || entry.type === "debate_turn_done") && entry.text) {
|
|
617
|
+
if ((entry.type === "delta" || entry.type === "user_message" || entry.type === "mention_user" || entry.type === "mention_response" || entry.type === "debate_turn_done" || entry.type === "debate_comment_injected") && entry.text) {
|
|
618
618
|
// Skip duplicate delta hits within the same assistant turn
|
|
619
619
|
if (entry.type === "delta" && currentTurnStart === lastAssistantHitTurn) continue;
|
|
620
620
|
var text = entry.text;
|