clay-server 2.20.1-beta.9 → 2.21.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/bin/cli.js +8 -3
- package/lib/daemon.js +11 -4
- package/lib/os-users.js +46 -0
- package/lib/sdk-bridge.js +111 -15
- package/lib/sdk-worker.js +40 -1
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -1641,7 +1641,7 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
|
|
|
1641
1641
|
// ==============================
|
|
1642
1642
|
// Dev mode — foreground daemon with file watching
|
|
1643
1643
|
// ==============================
|
|
1644
|
-
async function devMode(mode, keepAwake, existingPinHash) {
|
|
1644
|
+
async function devMode(mode, keepAwake, existingPinHash, wantOsUsers) {
|
|
1645
1645
|
var ip = getLocalIP();
|
|
1646
1646
|
var hasTls = false;
|
|
1647
1647
|
var hasBuiltinCert = false;
|
|
@@ -1723,6 +1723,7 @@ async function devMode(mode, keepAwake, existingPinHash) {
|
|
|
1723
1723
|
mode: mode || "single",
|
|
1724
1724
|
setupCompleted: true,
|
|
1725
1725
|
projects: allProjects,
|
|
1726
|
+
osUsers: wantOsUsers || (prevDevConfig ? (prevDevConfig.osUsers || false) : false),
|
|
1726
1727
|
};
|
|
1727
1728
|
|
|
1728
1729
|
ensureConfigDir();
|
|
@@ -2594,6 +2595,10 @@ function showSettingsMenu(config, ip) {
|
|
|
2594
2595
|
log(sym.bar + " " + a.dim + "Setting ACLs for " + cfgProjects.length + " project(s)..." + a.reset);
|
|
2595
2596
|
for (var pi = 0; pi < cfgProjects.length; pi++) {
|
|
2596
2597
|
var proj = cfgProjects[pi];
|
|
2598
|
+
if (osUsersLib.isHomeDirectory(proj.path)) {
|
|
2599
|
+
log(sym.bar + " " + a.dim + "~ " + (proj.slug || proj.path) + " (home dir, skipped)" + a.reset);
|
|
2600
|
+
continue;
|
|
2601
|
+
}
|
|
2597
2602
|
try {
|
|
2598
2603
|
if (proj.visibility === "public") {
|
|
2599
2604
|
osUsersLib.grantAllUsersAccess(proj.path, usersLib);
|
|
@@ -2873,11 +2878,11 @@ var currentVersion = require("../package.json").version;
|
|
|
2873
2878
|
// No config — go through setup (disclaimer, port, mode, etc.)
|
|
2874
2879
|
if (!devConfig) {
|
|
2875
2880
|
setup(function (mode, keepAwake, wantOsUsers) {
|
|
2876
|
-
devMode(mode, keepAwake, null);
|
|
2881
|
+
devMode(mode, keepAwake, null, wantOsUsers);
|
|
2877
2882
|
});
|
|
2878
2883
|
} else {
|
|
2879
2884
|
// Reuse existing config (repeat run)
|
|
2880
|
-
await devMode(devConfig.mode || "single", devConfig.keepAwake || false, devConfig.pinHash || null);
|
|
2885
|
+
await devMode(devConfig.mode || "single", devConfig.keepAwake || false, devConfig.pinHash || null, devConfig.osUsers || false);
|
|
2881
2886
|
}
|
|
2882
2887
|
return;
|
|
2883
2888
|
}
|
package/lib/daemon.js
CHANGED
|
@@ -1399,7 +1399,7 @@ function spawnAndRestart() {
|
|
|
1399
1399
|
var updateHandoff = false; // true when shutting down for update (new daemon already spawned)
|
|
1400
1400
|
|
|
1401
1401
|
function gracefulShutdown() {
|
|
1402
|
-
console.log("[daemon] Shutting down...");
|
|
1402
|
+
try { console.log("[daemon] Shutting down..."); } catch (e) {}
|
|
1403
1403
|
var exitCode = updateHandoff ? 120 : 0; // 120 = update handoff, don't auto-restart
|
|
1404
1404
|
|
|
1405
1405
|
if (caffeinateProc) {
|
|
@@ -1426,13 +1426,13 @@ function gracefulShutdown() {
|
|
|
1426
1426
|
}
|
|
1427
1427
|
|
|
1428
1428
|
relay.server.close(function () {
|
|
1429
|
-
console.log("[daemon] Server closed");
|
|
1429
|
+
try { console.log("[daemon] Server closed"); } catch (e) {}
|
|
1430
1430
|
process.exit(exitCode);
|
|
1431
1431
|
});
|
|
1432
1432
|
|
|
1433
1433
|
// Force exit after 5 seconds
|
|
1434
1434
|
setTimeout(function () {
|
|
1435
|
-
console.error("[daemon] Forced exit after timeout");
|
|
1435
|
+
try { console.error("[daemon] Forced exit after timeout"); } catch (e) {}
|
|
1436
1436
|
process.exit(1);
|
|
1437
1437
|
}, 5000);
|
|
1438
1438
|
}
|
|
@@ -1462,7 +1462,14 @@ process.on("uncaughtException", function (err) {
|
|
|
1462
1462
|
// A single session's SDK write was aborted (e.g. stream closed before
|
|
1463
1463
|
// write completed). This is recoverable, so do NOT tear down the whole
|
|
1464
1464
|
// daemon and kill every other session.
|
|
1465
|
-
console.error("[daemon] Suppressed AbortError (single-session failure):", errMsg);
|
|
1465
|
+
try { console.error("[daemon] Suppressed AbortError (single-session failure):", errMsg); } catch (e) {}
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// EIO/EPIPE on stdout/stderr when parent process (dev mode CLI) dies.
|
|
1470
|
+
// Not fatal for the daemon itself.
|
|
1471
|
+
var isIOError = errMsg.indexOf("EIO") !== -1 || errMsg.indexOf("EPIPE") !== -1;
|
|
1472
|
+
if (isIOError) {
|
|
1466
1473
|
return;
|
|
1467
1474
|
}
|
|
1468
1475
|
|
package/lib/os-users.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Used by sdk-bridge.js (worker spawning), terminal-manager.js, and project.js (file ops).
|
|
3
3
|
|
|
4
4
|
var fs = require("fs");
|
|
5
|
+
var path = require("path");
|
|
5
6
|
var { execSync } = require("child_process");
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -157,11 +158,30 @@ function checkAclSupport() {
|
|
|
157
158
|
}
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Check if a path is a user's home directory (e.g. /home/chad, /root).
|
|
163
|
+
* Running recursive setfacl on a home dir is dangerous and slow.
|
|
164
|
+
*/
|
|
165
|
+
function isHomeDirectory(dirPath) {
|
|
166
|
+
var resolved = path.resolve(dirPath);
|
|
167
|
+
// /root
|
|
168
|
+
if (resolved === "/root") return true;
|
|
169
|
+
// /home/username (exactly two levels)
|
|
170
|
+
var parts = resolved.split("/");
|
|
171
|
+
if (parts.length === 3 && parts[0] === "" && parts[1] === "home" && parts[2]) return true;
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
160
175
|
/**
|
|
161
176
|
* Grant a Linux user ACL access (rwX) to a project directory.
|
|
162
177
|
* Uses setfacl to add recursive + default ACL entries.
|
|
178
|
+
* Skips home directories to avoid slow recursive ACL on large trees.
|
|
163
179
|
*/
|
|
164
180
|
function grantProjectAccess(projectPath, linuxUser) {
|
|
181
|
+
if (isHomeDirectory(projectPath)) {
|
|
182
|
+
console.log("[os-users] Skipping ACL for home directory: " + projectPath);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
165
185
|
try {
|
|
166
186
|
// Recursive ACL for existing files
|
|
167
187
|
execSync("setfacl -R -m u:" + linuxUser + ":rwX " + JSON.stringify(projectPath), {
|
|
@@ -190,6 +210,10 @@ function grantProjectAccess(projectPath, linuxUser) {
|
|
|
190
210
|
* Revoke a Linux user's ACL access from a project directory.
|
|
191
211
|
*/
|
|
192
212
|
function revokeProjectAccess(projectPath, linuxUser) {
|
|
213
|
+
if (isHomeDirectory(projectPath)) {
|
|
214
|
+
console.log("[os-users] Skipping ACL revoke for home directory: " + projectPath);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
193
217
|
try {
|
|
194
218
|
execSync("setfacl -R -x u:" + linuxUser + " " + JSON.stringify(projectPath), {
|
|
195
219
|
encoding: "utf8",
|
|
@@ -242,6 +266,25 @@ function linuxUserExists(username) {
|
|
|
242
266
|
}
|
|
243
267
|
}
|
|
244
268
|
|
|
269
|
+
function getLinuxUserHome(username) {
|
|
270
|
+
try {
|
|
271
|
+
var line = execSync("getent passwd " + username, { encoding: "utf8", timeout: 5000, stdio: "pipe" }).trim();
|
|
272
|
+
var parts = line.split(":");
|
|
273
|
+
return parts[5] || "/home/" + username;
|
|
274
|
+
} catch (e) {
|
|
275
|
+
return "/home/" + username;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function getLinuxUserUid(username) {
|
|
280
|
+
try {
|
|
281
|
+
var uid = execSync("id -u " + username, { encoding: "utf8", timeout: 5000, stdio: "pipe" }).trim();
|
|
282
|
+
return parseInt(uid, 10);
|
|
283
|
+
} catch (e) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
245
288
|
/**
|
|
246
289
|
* Install Claude CLI for a Linux user account.
|
|
247
290
|
* Downloads and runs the install script, then ensures PATH is configured.
|
|
@@ -424,4 +467,7 @@ module.exports = {
|
|
|
424
467
|
installClaudeCli: installClaudeCli,
|
|
425
468
|
deactivateLinuxUser: deactivateLinuxUser,
|
|
426
469
|
ensureProjectsDir: ensureProjectsDir,
|
|
470
|
+
isHomeDirectory: isHomeDirectory,
|
|
471
|
+
getLinuxUserHome: getLinuxUserHome,
|
|
472
|
+
getLinuxUserUid: getLinuxUserUid,
|
|
427
473
|
};
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -613,22 +613,32 @@ function createSDKBridge(opts) {
|
|
|
613
613
|
var dirs = [];
|
|
614
614
|
while (dir !== path.dirname(dir)) {
|
|
615
615
|
dirs.push(dir);
|
|
616
|
-
// Stop once we leave the npm cache tree
|
|
617
|
-
if (dir.indexOf(".npm") === -1 && dir.indexOf("node_modules") === -1) break;
|
|
618
616
|
dir = path.dirname(dir);
|
|
619
617
|
}
|
|
618
|
+
// Open o+rx on each ancestor so non-root users can traverse the path
|
|
619
|
+
// (e.g. /root/.npm/_npx/.../node_modules/clay-server needs /root to be o+x)
|
|
620
620
|
for (var di = 0; di < dirs.length; di++) {
|
|
621
621
|
try {
|
|
622
622
|
var st = fs.statSync(dirs[di]);
|
|
623
|
-
// Add o+rx
|
|
624
|
-
|
|
625
|
-
|
|
623
|
+
// Add o+x (traverse) to all ancestors, o+rx to npm cache dirs
|
|
624
|
+
var isNpmDir = dirs[di].indexOf(".npm") !== -1 || dirs[di].indexOf("node_modules") !== -1;
|
|
625
|
+
var needed = isNpmDir ? 0o005 : 0o001; // rx for npm dirs, just x for ancestors like /root
|
|
626
|
+
if ((st.mode & needed) !== needed) {
|
|
627
|
+
fs.chmodSync(dirs[di], st.mode | needed);
|
|
626
628
|
}
|
|
627
629
|
} catch (e) {}
|
|
628
630
|
}
|
|
629
|
-
// Recursively make the package
|
|
631
|
+
// Recursively make the package AND hoisted dependencies readable.
|
|
632
|
+
// npm/npx may hoist deps (e.g. @anthropic-ai/claude-agent-sdk) to the
|
|
633
|
+
// parent node_modules/ instead of inside clay-server/node_modules/.
|
|
630
634
|
var { execSync: chmodExec } = require("child_process");
|
|
631
|
-
|
|
635
|
+
// Find the top-level node_modules that contains clay-server
|
|
636
|
+
var topNodeModules = path.join(pkgDir, "..");
|
|
637
|
+
if (path.basename(topNodeModules) === "node_modules") {
|
|
638
|
+
chmodExec("chmod -R o+rX " + JSON.stringify(topNodeModules), { stdio: "ignore", timeout: 15000 });
|
|
639
|
+
} else {
|
|
640
|
+
chmodExec("chmod -R o+rX " + JSON.stringify(pkgDir), { stdio: "ignore", timeout: 5000 });
|
|
641
|
+
}
|
|
632
642
|
} catch (e) {}
|
|
633
643
|
})();
|
|
634
644
|
|
|
@@ -697,15 +707,17 @@ function createSDKBridge(opts) {
|
|
|
697
707
|
// Set socket permissions so the target user can connect
|
|
698
708
|
try { fs.chmodSync(socketPath, 0o777); } catch (e) {}
|
|
699
709
|
|
|
700
|
-
// Spawn worker process as the target Linux user
|
|
701
|
-
|
|
710
|
+
// Spawn worker process as the target Linux user.
|
|
711
|
+
// Inherit full env from daemon, override user-specific vars.
|
|
712
|
+
var workerEnv = Object.assign({}, process.env, {
|
|
702
713
|
HOME: userInfo.home,
|
|
703
714
|
USER: linuxUser,
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
LANG: process.env.LANG || "en_US.UTF-8",
|
|
707
|
-
};
|
|
715
|
+
LOGNAME: linuxUser,
|
|
716
|
+
});
|
|
708
717
|
|
|
718
|
+
console.log("[sdk-bridge] Spawning worker: uid=" + userInfo.uid + " gid=" + userInfo.gid + " cwd=" + cwd + " socket=" + socketPath);
|
|
719
|
+
console.log("[sdk-bridge] Worker script: " + WORKER_SCRIPT);
|
|
720
|
+
console.log("[sdk-bridge] Node: " + process.execPath);
|
|
709
721
|
worker.process = spawn(process.execPath, [WORKER_SCRIPT, socketPath], {
|
|
710
722
|
uid: userInfo.uid,
|
|
711
723
|
gid: userInfo.gid,
|
|
@@ -725,8 +737,24 @@ function createSDKBridge(opts) {
|
|
|
725
737
|
});
|
|
726
738
|
|
|
727
739
|
worker.process.on("exit", function(code, signal) {
|
|
728
|
-
console.log("[sdk-bridge] Worker for " + linuxUser + " exited (code=" + code + ", signal=" + signal + ")");
|
|
740
|
+
console.log("[sdk-bridge] Worker for " + linuxUser + " exited (code=" + code + ", signal=" + signal + ")" + (worker._stderrBuf ? " stderr: " + worker._stderrBuf.trim() : ""));
|
|
741
|
+
// Reject readyPromise if worker dies before becoming ready
|
|
742
|
+
if (!worker.ready && worker._readyResolve) {
|
|
743
|
+
worker._readyResolve = null;
|
|
744
|
+
// Let the readyPromise hang; the query_error handler will clean up
|
|
745
|
+
}
|
|
729
746
|
// Notify message handlers about unexpected exit so sessions don't hang
|
|
747
|
+
if (code === 0 && !worker.ready) {
|
|
748
|
+
// Worker exited cleanly before sending "ready" — something is wrong
|
|
749
|
+
for (var h = 0; h < worker.messageHandlers.length; h++) {
|
|
750
|
+
worker.messageHandlers[h]({
|
|
751
|
+
type: "query_error",
|
|
752
|
+
error: "Worker exited before ready (code=0). stderr: " + (worker._stderrBuf || "(none)"),
|
|
753
|
+
exitCode: 0,
|
|
754
|
+
stderr: worker._stderrBuf || null,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
}
|
|
730
758
|
if (code !== 0 && code !== null) {
|
|
731
759
|
var stderrText = worker._stderrBuf || "";
|
|
732
760
|
for (var h = 0; h < worker.messageHandlers.length; h++) {
|
|
@@ -786,6 +814,9 @@ function createSDKBridge(opts) {
|
|
|
786
814
|
* Mirrors the in-process startQuery flow but delegates SDK execution to the worker.
|
|
787
815
|
*/
|
|
788
816
|
async function startQueryViaWorker(session, text, images, linuxUser) {
|
|
817
|
+
// Save for auto-retry on session-not-found
|
|
818
|
+
session._lastQueryText = text;
|
|
819
|
+
session._lastQueryImages = images;
|
|
789
820
|
var worker;
|
|
790
821
|
try {
|
|
791
822
|
worker = spawnWorker(linuxUser);
|
|
@@ -929,7 +960,69 @@ function createSDKBridge(opts) {
|
|
|
929
960
|
}
|
|
930
961
|
break;
|
|
931
962
|
|
|
932
|
-
case "query_error":
|
|
963
|
+
case "query_error": {
|
|
964
|
+
// Check session-not-found before isProcessing gate (it can arrive after processing is cleared)
|
|
965
|
+
var qerrLower = (msg.error || "").toLowerCase();
|
|
966
|
+
var isSessionNotFound = qerrLower.indexOf("no conversation found") !== -1
|
|
967
|
+
|| qerrLower.indexOf("session not found") !== -1;
|
|
968
|
+
if (isSessionNotFound && !session._sessionMigrateRetried) {
|
|
969
|
+
// Auto-retry: copy CLI session file to OS user's home, then resume
|
|
970
|
+
console.log("[sdk-bridge] Session not found for OS user, migrating CLI session for session " + session.localId);
|
|
971
|
+
session._sessionMigrateRetried = true;
|
|
972
|
+
// Try to copy the CLI session file from original user to OS user
|
|
973
|
+
var migratedOk = false;
|
|
974
|
+
if (session.cliSessionId && session.lastLinuxUser) {
|
|
975
|
+
try {
|
|
976
|
+
var configMod = require("./config");
|
|
977
|
+
var osUsersMod = require("./os-users");
|
|
978
|
+
var originalHome = configMod.REAL_HOME || require("os").homedir();
|
|
979
|
+
var linuxUserHome = osUsersMod.getLinuxUserHome ? osUsersMod.getLinuxUserHome(session.lastLinuxUser) : "/home/" + session.lastLinuxUser;
|
|
980
|
+
var projectDir = session.cwd || "";
|
|
981
|
+
var projectSlug = projectDir.replace(/\//g, "-");
|
|
982
|
+
var srcFile = path.join(originalHome, ".claude", "projects", projectSlug, session.cliSessionId + ".jsonl");
|
|
983
|
+
var dstDir = path.join(linuxUserHome, ".claude", "projects", projectSlug);
|
|
984
|
+
var dstFile = path.join(dstDir, session.cliSessionId + ".jsonl");
|
|
985
|
+
if (fs.existsSync(srcFile) && !fs.existsSync(dstFile)) {
|
|
986
|
+
fs.mkdirSync(dstDir, { recursive: true });
|
|
987
|
+
fs.copyFileSync(srcFile, dstFile);
|
|
988
|
+
// Fix ownership so the OS user can read/write it
|
|
989
|
+
var uid = osUsersMod.getLinuxUserUid ? osUsersMod.getLinuxUserUid(session.lastLinuxUser) : null;
|
|
990
|
+
if (uid != null) {
|
|
991
|
+
try { require("child_process").execSync("chown -R " + uid + " " + JSON.stringify(dstDir)); } catch (e2) {}
|
|
992
|
+
}
|
|
993
|
+
console.log("[sdk-bridge] Copied CLI session " + session.cliSessionId + " to " + dstFile);
|
|
994
|
+
migratedOk = true;
|
|
995
|
+
} else if (fs.existsSync(dstFile)) {
|
|
996
|
+
migratedOk = true; // already there
|
|
997
|
+
}
|
|
998
|
+
} catch (copyErr) {
|
|
999
|
+
console.error("[sdk-bridge] Failed to copy CLI session:", copyErr.message);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
// Detach old worker so its exit event doesn't interfere with the new one
|
|
1003
|
+
if (session.worker) {
|
|
1004
|
+
session.worker.messageHandlers = [];
|
|
1005
|
+
session.worker.kill();
|
|
1006
|
+
session.worker = null;
|
|
1007
|
+
}
|
|
1008
|
+
session.queryInstance = null;
|
|
1009
|
+
session.messageQueue = null;
|
|
1010
|
+
session.abortController = null;
|
|
1011
|
+
// Ensure client knows we're still processing (re-send status)
|
|
1012
|
+
session.isProcessing = true;
|
|
1013
|
+
onProcessingChanged();
|
|
1014
|
+
send({ type: "status", status: "processing" });
|
|
1015
|
+
// If migration failed, clear cliSessionId so it starts fresh
|
|
1016
|
+
if (!migratedOk) {
|
|
1017
|
+
session.cliSessionId = null;
|
|
1018
|
+
}
|
|
1019
|
+
// Re-run the query (with resume if migration succeeded)
|
|
1020
|
+
var retryText = session._lastQueryText || "";
|
|
1021
|
+
var retryImages = session._lastQueryImages || undefined;
|
|
1022
|
+
var retryLinuxUser = session.lastLinuxUser || null;
|
|
1023
|
+
startQuery(session, retryText, retryImages, retryLinuxUser);
|
|
1024
|
+
break;
|
|
1025
|
+
}
|
|
933
1026
|
if (session.isProcessing) {
|
|
934
1027
|
session.isProcessing = false;
|
|
935
1028
|
onProcessingChanged();
|
|
@@ -1014,6 +1107,7 @@ function createSDKBridge(opts) {
|
|
|
1014
1107
|
}
|
|
1015
1108
|
}
|
|
1016
1109
|
break;
|
|
1110
|
+
}
|
|
1017
1111
|
|
|
1018
1112
|
case "model_changed":
|
|
1019
1113
|
sm.currentModel = msg.model;
|
|
@@ -1055,6 +1149,8 @@ function createSDKBridge(opts) {
|
|
|
1055
1149
|
prompt: initialMessage,
|
|
1056
1150
|
options: queryOptions,
|
|
1057
1151
|
singleTurn: !!session.singleTurn,
|
|
1152
|
+
originalHome: require("./config").REAL_HOME || null,
|
|
1153
|
+
projectPath: session.cwd || null,
|
|
1058
1154
|
});
|
|
1059
1155
|
}
|
|
1060
1156
|
|
package/lib/sdk-worker.js
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Usage: node sdk-worker.js <socket-path>
|
|
6
6
|
|
|
7
|
+
// Early diagnostic — writes directly to fd 2 to ensure output even if pipes close fast
|
|
8
|
+
try { require("fs").writeSync(2, "[sdk-worker] BOOT pid=" + process.pid + " uid=" + (typeof process.getuid === "function" ? process.getuid() : "?") + " argv=" + process.argv.slice(1).join(" ") + "\n"); } catch (e) {}
|
|
9
|
+
|
|
7
10
|
var net = require("net");
|
|
8
11
|
var crypto = require("crypto");
|
|
9
12
|
var path = require("path");
|
|
@@ -217,6 +220,31 @@ async function handleQueryStart(msg) {
|
|
|
217
220
|
messageQueue.push(msg.prompt);
|
|
218
221
|
}
|
|
219
222
|
|
|
223
|
+
// If resuming a session that doesn't exist for this OS user, try to migrate
|
|
224
|
+
// the session file from the original user's home directory.
|
|
225
|
+
if (msg.options && msg.options.resume && msg.originalHome && msg.projectPath) {
|
|
226
|
+
try {
|
|
227
|
+
var fs = require("fs");
|
|
228
|
+
var homePath = require("os").homedir();
|
|
229
|
+
if (homePath !== msg.originalHome) {
|
|
230
|
+
var projDirName = msg.projectPath.replace(/\//g, "-");
|
|
231
|
+
var sessionFile = msg.options.resume + ".jsonl";
|
|
232
|
+
var destDir = path.join(homePath, ".claude", "projects", projDirName);
|
|
233
|
+
var destFile = path.join(destDir, sessionFile);
|
|
234
|
+
if (!fs.existsSync(destFile)) {
|
|
235
|
+
var srcFile = path.join(msg.originalHome, ".claude", "projects", projDirName, sessionFile);
|
|
236
|
+
if (fs.existsSync(srcFile)) {
|
|
237
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
238
|
+
fs.copyFileSync(srcFile, destFile);
|
|
239
|
+
try { fs.writeSync(2, "[sdk-worker] Migrated session " + msg.options.resume + " from " + msg.originalHome + "\n"); } catch (e2) {}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
} catch (migrateErr) {
|
|
244
|
+
try { fs.writeSync(2, "[sdk-worker] Session migration failed: " + migrateErr.message + "\n"); } catch (e2) {}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
220
248
|
// Build query options (callbacks are local, everything else from daemon)
|
|
221
249
|
var options = msg.options || {};
|
|
222
250
|
options.abortController = abortController;
|
|
@@ -391,6 +419,9 @@ async function handleWarmup(msg) {
|
|
|
391
419
|
|
|
392
420
|
// --- Cleanup ---
|
|
393
421
|
function cleanup() {
|
|
422
|
+
if (_keepAlive) {
|
|
423
|
+
try { clearInterval(_keepAlive); } catch (e) {}
|
|
424
|
+
}
|
|
394
425
|
if (abortController) {
|
|
395
426
|
try { abortController.abort(); } catch (e) {}
|
|
396
427
|
}
|
|
@@ -402,8 +433,14 @@ function cleanup() {
|
|
|
402
433
|
}
|
|
403
434
|
}
|
|
404
435
|
|
|
436
|
+
// Keep event loop alive — without this, Node may exit if the socket handle
|
|
437
|
+
// gets unreferenced (observed on Linux with uid/gid spawn)
|
|
438
|
+
var _keepAlive = setInterval(function() {}, 30000);
|
|
439
|
+
|
|
405
440
|
// --- Connect to daemon socket ---
|
|
441
|
+
try { require("fs").writeSync(2, "[sdk-worker] Connecting to socket: " + socketPath + "\n"); } catch (e) {}
|
|
406
442
|
conn = net.connect(socketPath, function() {
|
|
443
|
+
try { require("fs").writeSync(2, "[sdk-worker] Connected, sending ready\n"); } catch (e) {}
|
|
407
444
|
sendToDaemon({ type: "ready" });
|
|
408
445
|
});
|
|
409
446
|
|
|
@@ -429,18 +466,20 @@ conn.on("error", function(err) {
|
|
|
429
466
|
});
|
|
430
467
|
|
|
431
468
|
conn.on("close", function() {
|
|
432
|
-
|
|
469
|
+
try { require("fs").writeSync(2, "[sdk-worker] EXIT REASON: socket closed\n"); } catch (e) {}
|
|
433
470
|
cleanup();
|
|
434
471
|
process.exit(0);
|
|
435
472
|
});
|
|
436
473
|
|
|
437
474
|
// Handle process signals
|
|
438
475
|
process.on("SIGTERM", function() {
|
|
476
|
+
try { require("fs").writeSync(2, "[sdk-worker] EXIT REASON: SIGTERM\n"); } catch (e) {}
|
|
439
477
|
cleanup();
|
|
440
478
|
process.exit(0);
|
|
441
479
|
});
|
|
442
480
|
|
|
443
481
|
process.on("SIGINT", function() {
|
|
482
|
+
try { require("fs").writeSync(2, "[sdk-worker] EXIT REASON: SIGINT\n"); } catch (e) {}
|
|
444
483
|
cleanup();
|
|
445
484
|
process.exit(0);
|
|
446
485
|
});
|