clay-server 2.33.1 → 2.34.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/ask-user-mcp-server.js +120 -0
- package/lib/config.js +9 -13
- package/lib/daemon.js +116 -55
- package/lib/mate-datastore.js +359 -0
- package/lib/mates.js +2 -2
- package/lib/os-users.js +70 -37
- package/lib/project-connection.js +16 -9
- package/lib/project-http.js +3 -4
- package/lib/project-image.js +3 -2
- package/lib/project-mate-datastore.js +232 -0
- package/lib/project-sessions.js +110 -7
- package/lib/project-user-message.js +4 -3
- package/lib/project.js +126 -10
- package/lib/public/app.js +2 -0
- package/lib/public/css/mates.css +228 -11
- package/lib/public/css/messages.css +23 -0
- package/lib/public/css/mobile-nav.css +0 -14
- package/lib/public/css/notifications-center.css +80 -0
- package/lib/public/css/sidebar.css +326 -101
- package/lib/public/index.html +24 -29
- package/lib/public/modules/app-dm.js +0 -2
- package/lib/public/modules/app-messages.js +23 -0
- package/lib/public/modules/app-rendering.js +0 -2
- package/lib/public/modules/diff.js +21 -7
- package/lib/public/modules/mate-datastore-ui.js +280 -0
- package/lib/public/modules/mate-sidebar.js +3 -9
- package/lib/public/modules/mate-wizard.js +15 -15
- package/lib/public/modules/sidebar-mobile.js +10 -20
- package/lib/public/modules/sidebar-sessions.js +490 -113
- package/lib/public/modules/sidebar.js +8 -6
- package/lib/public/modules/tools.js +115 -18
- package/lib/public/sw.js +1 -1
- package/lib/sdk-bridge.js +56 -41
- package/lib/sdk-message-processor.js +21 -4
- package/lib/server.js +28 -72
- package/lib/sessions.js +157 -20
- package/lib/updater.js +2 -2
- package/lib/users.js +2 -2
- package/lib/ws-schema.js +16 -0
- package/lib/yoke/adapters/claude-worker.js +114 -2
- package/lib/yoke/adapters/claude.js +56 -5
- package/lib/yoke/adapters/codex.js +350 -58
- package/lib/yoke/index.js +93 -48
- package/lib/yoke/instructions.js +0 -1
- package/lib/yoke/mcp-bridge-server.js +14 -6
- package/package.json +1 -2
- package/lib/yoke/adapters/gemini.js +0 -709
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Ask User MCP Server for Clay
|
|
2
|
+
// Provides a mate-only ask_user_questions tool that reuses the existing
|
|
3
|
+
// AskUserQuestion UI and ask_user_response flow.
|
|
4
|
+
|
|
5
|
+
var z;
|
|
6
|
+
try { z = require("zod"); } catch (e) { z = null; }
|
|
7
|
+
|
|
8
|
+
// Returns a Zod "shape" object (property -> zod field) matching what
|
|
9
|
+
// Claude SDK's `sdk.tool()` expects. Do NOT wrap in z.object() here —
|
|
10
|
+
// the SDK does that internally.
|
|
11
|
+
//
|
|
12
|
+
// The schema is deliberately tight: options are required (2-6), each with
|
|
13
|
+
// a label and a short description. This pushes Codex (which treats the
|
|
14
|
+
// schema as a loose hint) toward producing well-structured multiple-choice
|
|
15
|
+
// cards instead of a bare "Other..." text field.
|
|
16
|
+
function buildQuestionShape() {
|
|
17
|
+
if (!z) return {};
|
|
18
|
+
|
|
19
|
+
var optionSchema = z.object({
|
|
20
|
+
label: z.string().min(1).max(60)
|
|
21
|
+
.describe("Short button label, 1-6 words. Shown as the primary option text."),
|
|
22
|
+
description: z.string().min(1).max(160)
|
|
23
|
+
.describe("One-line clarifier shown under the label. Concrete example or scope."),
|
|
24
|
+
markdown: z.string().optional()
|
|
25
|
+
.describe("Optional longer markdown body shown on expand. Use sparingly."),
|
|
26
|
+
}).passthrough();
|
|
27
|
+
|
|
28
|
+
var questionSchema = z.object({
|
|
29
|
+
header: z.string().min(1).max(40)
|
|
30
|
+
.describe("Short ALL-CAPS-ish section header above the question (e.g. 'FIRST THINGS FIRST', 'SCOPE'). Gives the user context for this step."),
|
|
31
|
+
question: z.string().min(1)
|
|
32
|
+
.describe("The actual question in natural spoken tone. Be specific, not generic."),
|
|
33
|
+
multiSelect: z.boolean().optional()
|
|
34
|
+
.describe("Set true only when multiple answers genuinely make sense. Default false."),
|
|
35
|
+
options: z.array(optionSchema).min(2).max(6)
|
|
36
|
+
.describe("Concrete options. Preferred count is 4; acceptable range is 2-6. FOUR is the target. Do NOT default to 3 out of habit. If you can only think of 3, push yourself: add a scope variant, edge case, or 'combined' option as the 4th. Only drop below 4 when the answer space is genuinely narrower (e.g. yes/no/unsure). Never include an 'Other' option yourself (the UI renders one automatically)."),
|
|
37
|
+
}).passthrough();
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
questions: z.array(questionSchema).min(1).max(3)
|
|
41
|
+
.describe("One to three question objects to show together as a card. Prefer ONE focused question per call."),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
var TOOL_DESCRIPTION = [
|
|
46
|
+
"Ask the user a structured multiple-choice question. Renders as a card with a header, the question, 2-6 clickable option buttons, and an always-present 'Other...' free-text field.",
|
|
47
|
+
"",
|
|
48
|
+
"WHEN TO USE:",
|
|
49
|
+
"- Interviewing the user at mate setup, or whenever you need to narrow scope before acting.",
|
|
50
|
+
"- Any branching decision where 2-6 concrete choices cover most of the space.",
|
|
51
|
+
"",
|
|
52
|
+
"REQUIRED STRUCTURE (all fields matter, do not skip):",
|
|
53
|
+
"- header: short section label, like 'FIRST THINGS FIRST' or 'SCOPE'. Gives context.",
|
|
54
|
+
"- question: one specific question in natural tone. Avoid generic 'how can I help?'.",
|
|
55
|
+
"- options: ALWAYS provide FOUR options by default. Acceptable range is 4-6; only drop to 3 if the answer space is genuinely binary-plus-one (rare). Each option has a short label (1-6 words) AND a one-line description giving a concrete example or scope. The UI already renders an 'Other...' text field, so you never need to include an 'Other' option yourself.",
|
|
56
|
+
"- multiSelect: omit or false unless multiple answers clearly apply.",
|
|
57
|
+
"",
|
|
58
|
+
"ON OPTION COUNT (IMPORTANT):",
|
|
59
|
+
"- Default to 4 options. Not 3. Models tend to gravitate to 3 because it feels tidy; resist that.",
|
|
60
|
+
"- If you're about to produce 3 options, stop and think: is there a fourth axis you're missing? A scope variant? A 'both' or 'neither'? A more niche case? Add it.",
|
|
61
|
+
"- 3 is only acceptable when the fourth option would be truly degenerate (e.g. yes/no/unsure).",
|
|
62
|
+
"- 5 or 6 is fine when the space is wide; don't cram.",
|
|
63
|
+
"",
|
|
64
|
+
"GOOD EXAMPLE:",
|
|
65
|
+
' { header: "FIRST THINGS FIRST",',
|
|
66
|
+
' question: "\'Language\' is broad, what do you actually want help with?",',
|
|
67
|
+
' options: [',
|
|
68
|
+
' { label: "A new language from scratch", description: "Pick up a language you don\'t speak yet (e.g. Japanese, Spanish)" },',
|
|
69
|
+
' { label: "Sharpen English", description: "Level up your English, writing, speaking, nuance, executive communication" },',
|
|
70
|
+
' { label: "Sharpen Korean", description: "Polish your Korean, writing style, formal register, etc." },',
|
|
71
|
+
' { label: "Teach me how to teach", description: "Help you teach language to others, like Elyse or team members" }',
|
|
72
|
+
" ] }",
|
|
73
|
+
"",
|
|
74
|
+
"BAD EXAMPLES (do not do this):",
|
|
75
|
+
"- Asking a vague question with no options and relying on the Other field.",
|
|
76
|
+
"- Options with only labels and no descriptions.",
|
|
77
|
+
"- Defaulting to exactly 3 options out of habit. Aim for 4.",
|
|
78
|
+
"- More than one question per call unless they are tightly related.",
|
|
79
|
+
"- Single-option 'questions' (use a plain message instead).",
|
|
80
|
+
].join("\n");
|
|
81
|
+
|
|
82
|
+
function getToolDefs(onAsk) {
|
|
83
|
+
var tools = [];
|
|
84
|
+
|
|
85
|
+
tools.push({
|
|
86
|
+
name: "ask_user_questions",
|
|
87
|
+
description: TOOL_DESCRIPTION,
|
|
88
|
+
inputSchema: buildQuestionShape(),
|
|
89
|
+
handler: function (args) {
|
|
90
|
+
if (!args || !Array.isArray(args.questions) || args.questions.length === 0) {
|
|
91
|
+
return Promise.resolve({
|
|
92
|
+
content: [{ type: "text", text: "Error: questions must be a non-empty array." }],
|
|
93
|
+
isError: true,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// Defensive structural check only: zod already enforces min 2.
|
|
97
|
+
// We do NOT reject on "only 3 options" — that's a soft preference
|
|
98
|
+
// expressed via the tool description, not a hard validation rule.
|
|
99
|
+
for (var i = 0; i < args.questions.length; i++) {
|
|
100
|
+
var q = args.questions[i];
|
|
101
|
+
if (!q || !Array.isArray(q.options) || q.options.length < 2) {
|
|
102
|
+
return Promise.resolve({
|
|
103
|
+
content: [{
|
|
104
|
+
type: "text",
|
|
105
|
+
text: "Error: question " + (i + 1) + " must include at least 2 options. "
|
|
106
|
+
+ "Provide concrete { label, description } choices (4 preferred). "
|
|
107
|
+
+ "The UI already shows an 'Other...' free-text field, so never add an 'Other' option yourself.",
|
|
108
|
+
}],
|
|
109
|
+
isError: true,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return onAsk(args);
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return tools;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { getToolDefs: getToolDefs };
|
package/lib/config.js
CHANGED
|
@@ -2,31 +2,27 @@ var fs = require("fs");
|
|
|
2
2
|
var path = require("path");
|
|
3
3
|
var os = require("os");
|
|
4
4
|
var net = require("net");
|
|
5
|
+
var execFileSync = require("child_process").execFileSync;
|
|
6
|
+
|
|
7
|
+
function isSafeSystemUserName(name) {
|
|
8
|
+
return typeof name === "string" && /^[a-z_][a-z0-9_-]*[$]?$/.test(name);
|
|
9
|
+
}
|
|
5
10
|
|
|
6
11
|
// When running under sudo, resolve the real user's home directory
|
|
7
12
|
// so that ~/.clay/ points to the original user's data, not /root/.clay/
|
|
8
13
|
function getRealHome() {
|
|
9
14
|
var sudoUser = process.env.SUDO_USER;
|
|
10
|
-
if (sudoUser && sudoUser !== "root") {
|
|
15
|
+
if (sudoUser && sudoUser !== "root" && isSafeSystemUserName(sudoUser)) {
|
|
11
16
|
// 1. Try getent passwd (works on most Linux, may fail with some NSS configs)
|
|
12
17
|
try {
|
|
13
|
-
var entry =
|
|
14
|
-
.execSync("getent passwd " + sudoUser, { encoding: "utf8", timeout: 3000 })
|
|
15
|
-
.trim();
|
|
18
|
+
var entry = execFileSync("getent", ["passwd", sudoUser], { encoding: "utf8", timeout: 3000 }).trim();
|
|
16
19
|
var home = entry.split(":")[5];
|
|
17
20
|
if (home && fs.existsSync(home)) return home;
|
|
18
21
|
} catch (e) {}
|
|
19
|
-
// 2.
|
|
20
|
-
try {
|
|
21
|
-
var home = require("child_process")
|
|
22
|
-
.execSync("eval echo ~" + sudoUser, { encoding: "utf8", timeout: 3000 })
|
|
23
|
-
.trim();
|
|
24
|
-
if (home && home !== "~" + sudoUser && fs.existsSync(home)) return home;
|
|
25
|
-
} catch (e2) {}
|
|
26
|
-
// 3. Direct path fallback (GCE, cloud VMs)
|
|
22
|
+
// 2. Direct path fallback (GCE, cloud VMs)
|
|
27
23
|
var directHome = "/home/" + sudoUser;
|
|
28
24
|
if (fs.existsSync(directHome)) return directHome;
|
|
29
|
-
//
|
|
25
|
+
// 3. SUDO_USER's original HOME (some sudo configs preserve it)
|
|
30
26
|
if (process.env.SUDO_HOME && fs.existsSync(process.env.SUDO_HOME)) return process.env.SUDO_HOME;
|
|
31
27
|
}
|
|
32
28
|
return os.homedir();
|
package/lib/daemon.js
CHANGED
|
@@ -53,11 +53,11 @@ console.log("[daemon] UID: " + (typeof process.getuid === "function" ? process.g
|
|
|
53
53
|
|
|
54
54
|
// --- OS users mode: check required system dependencies ---
|
|
55
55
|
if (config.osUsers) {
|
|
56
|
-
var
|
|
56
|
+
var checkExec = require("child_process").execFileSync;
|
|
57
57
|
var missing = [];
|
|
58
|
-
try { checkExec("which setfacl", { stdio: "ignore" }); } catch (e) { missing.push("acl (setfacl)"); }
|
|
59
|
-
try { checkExec("which git", { stdio: "ignore" }); } catch (e) { missing.push("git"); }
|
|
60
|
-
try { checkExec("which useradd", { stdio: "ignore" }); } catch (e) { missing.push("useradd"); }
|
|
58
|
+
try { checkExec("which", ["setfacl"], { stdio: "ignore" }); } catch (e) { missing.push("acl (setfacl)"); }
|
|
59
|
+
try { checkExec("which", ["git"], { stdio: "ignore" }); } catch (e) { missing.push("git"); }
|
|
60
|
+
try { checkExec("which", ["useradd"], { stdio: "ignore" }); } catch (e) { missing.push("useradd"); }
|
|
61
61
|
if (missing.length > 0) {
|
|
62
62
|
console.error("[daemon] OS users mode requires missing system packages: " + missing.join(", "));
|
|
63
63
|
console.error("[daemon] Install with: sudo apt install " + missing.map(function (m) { return m.split(" ")[0]; }).join(" "));
|
|
@@ -179,7 +179,7 @@ var relay = createServer({
|
|
|
179
179
|
onCreateProject: function (projectName, wsUser) {
|
|
180
180
|
console.log("[daemon] onCreateProject wsUser:", JSON.stringify(wsUser ? { id: wsUser.id, role: wsUser.role, username: wsUser.username, linuxUser: wsUser.linuxUser } : null));
|
|
181
181
|
var os = require("os");
|
|
182
|
-
var
|
|
182
|
+
var execFileSync = require("child_process").execFileSync;
|
|
183
183
|
var baseDir;
|
|
184
184
|
if (config.osUsers) {
|
|
185
185
|
baseDir = "/var/clay/projects";
|
|
@@ -199,21 +199,23 @@ var relay = createServer({
|
|
|
199
199
|
if (linuxUser) {
|
|
200
200
|
var uidGid = null;
|
|
201
201
|
try {
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
uidGid = {
|
|
203
|
+
uid: parseInt(execFileSync("id", ["-u", linuxUser], { encoding: "utf8", stdio: "pipe" }).trim(), 10),
|
|
204
|
+
gid: parseInt(execFileSync("id", ["-g", linuxUser], { encoding: "utf8", stdio: "pipe" }).trim(), 10),
|
|
205
|
+
};
|
|
204
206
|
} catch (e) {}
|
|
205
207
|
if (uidGid) {
|
|
206
208
|
fs.chmodSync(targetDir, 0o700);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
+
execFileSync("chown", ["-R", linuxUser + ":" + linuxUser, targetDir]);
|
|
210
|
+
execFileSync("git", ["init"], { cwd: targetDir, uid: uidGid.uid, gid: uidGid.gid, env: { PATH: "/usr/local/bin:/usr/bin:/bin" } });
|
|
209
211
|
} else {
|
|
210
|
-
|
|
212
|
+
execFileSync("git", ["init"], { cwd: targetDir });
|
|
211
213
|
}
|
|
212
214
|
} else {
|
|
213
|
-
|
|
215
|
+
execFileSync("git", ["init"], { cwd: targetDir });
|
|
214
216
|
}
|
|
215
217
|
} else {
|
|
216
|
-
|
|
218
|
+
execFileSync("git", ["init"], { cwd: targetDir });
|
|
217
219
|
}
|
|
218
220
|
} catch (e) {
|
|
219
221
|
try { fs.rmSync(targetDir, { recursive: true, force: true }); } catch (ce) {}
|
|
@@ -245,7 +247,8 @@ var relay = createServer({
|
|
|
245
247
|
},
|
|
246
248
|
onCloneProject: function (cloneUrl, wsUser, callback) {
|
|
247
249
|
var os = require("os");
|
|
248
|
-
var
|
|
250
|
+
var spawn = require("child_process").spawn;
|
|
251
|
+
var execFileSync = require("child_process").execFileSync;
|
|
249
252
|
var baseDir;
|
|
250
253
|
if (config.osUsers) {
|
|
251
254
|
baseDir = "/var/clay/projects";
|
|
@@ -262,9 +265,8 @@ var relay = createServer({
|
|
|
262
265
|
var spawnOpts = { cwd: baseDir };
|
|
263
266
|
if (config.osUsers && wsUser && wsUser.linuxUser) {
|
|
264
267
|
try {
|
|
265
|
-
|
|
266
|
-
spawnOpts.
|
|
267
|
-
spawnOpts.gid = parseInt(passwdLine[1], 10);
|
|
268
|
+
spawnOpts.uid = parseInt(execFileSync("id", ["-u", wsUser.linuxUser], { encoding: "utf8", stdio: "pipe" }).trim(), 10);
|
|
269
|
+
spawnOpts.gid = parseInt(execFileSync("id", ["-g", wsUser.linuxUser], { encoding: "utf8", stdio: "pipe" }).trim(), 10);
|
|
268
270
|
spawnOpts.env = Object.assign({}, process.env, {
|
|
269
271
|
HOME: "/home/" + wsUser.linuxUser,
|
|
270
272
|
USER: wsUser.linuxUser
|
|
@@ -304,7 +306,7 @@ var relay = createServer({
|
|
|
304
306
|
if (config.osUsers && wsUser && wsUser.linuxUser) {
|
|
305
307
|
try {
|
|
306
308
|
fs.chmodSync(targetDir, 0o700);
|
|
307
|
-
|
|
309
|
+
execFileSync("chown", ["-R", wsUser.linuxUser + ":" + wsUser.linuxUser, targetDir]);
|
|
308
310
|
} catch (e) {}
|
|
309
311
|
}
|
|
310
312
|
// Register project - creator always becomes owner
|
|
@@ -902,6 +904,47 @@ var relay = createServer({
|
|
|
902
904
|
},
|
|
903
905
|
});
|
|
904
906
|
|
|
907
|
+
var IDLE_MS = Number(process.env.CLAY_CODEX_IDLE_MS) || 5 * 60 * 1000;
|
|
908
|
+
var REAP_MS = Number(process.env.CLAY_CODEX_REAPER_MS) || 60 * 1000;
|
|
909
|
+
var reaperHandle = setInterval(function () {
|
|
910
|
+
if (!relay || typeof relay.forEachProject !== "function") return;
|
|
911
|
+
relay.forEachProject(function (ctx) {
|
|
912
|
+
try {
|
|
913
|
+
if (ctx && ctx.adapters && ctx.adapters.codex && typeof ctx.adapters.codex.shutdownIfIdle === "function") {
|
|
914
|
+
var result = ctx.adapters.codex.shutdownIfIdle(IDLE_MS);
|
|
915
|
+
if (result && typeof result.catch === "function") {
|
|
916
|
+
result.catch(function (e) {
|
|
917
|
+
console.error("[daemon] Codex idle reclaim failed:", e && e.message ? e.message : e);
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
} catch (e) {}
|
|
922
|
+
});
|
|
923
|
+
}, REAP_MS);
|
|
924
|
+
if (reaperHandle && typeof reaperHandle.unref === "function") {
|
|
925
|
+
reaperHandle.unref();
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function stopCodexReaper() {
|
|
929
|
+
if (reaperHandle) {
|
|
930
|
+
clearInterval(reaperHandle);
|
|
931
|
+
reaperHandle = null;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function shutdownProjects() {
|
|
936
|
+
stopCodexReaper();
|
|
937
|
+
try {
|
|
938
|
+
var result = relay.destroyAll();
|
|
939
|
+
if (result && typeof result.then === "function") {
|
|
940
|
+
return result;
|
|
941
|
+
}
|
|
942
|
+
return Promise.resolve(true);
|
|
943
|
+
} catch (e) {
|
|
944
|
+
return Promise.reject(e);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
905
948
|
// Worktree tracking extracted to daemon-projects.js
|
|
906
949
|
|
|
907
950
|
// --- Register projects ---
|
|
@@ -1303,41 +1346,48 @@ function spawnAndRestart() {
|
|
|
1303
1346
|
try {
|
|
1304
1347
|
updateHandoff = true;
|
|
1305
1348
|
ipc.close();
|
|
1306
|
-
|
|
1307
|
-
|
|
1349
|
+
shutdownProjects().then(function () {
|
|
1350
|
+
if (relay.onboardingServer) relay.onboardingServer.close();
|
|
1308
1351
|
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1352
|
+
// Close the server first so the port is released before spawning the new daemon
|
|
1353
|
+
relay.server.close(function () {
|
|
1354
|
+
try {
|
|
1355
|
+
var { spawn: spawnRestart } = require("child_process");
|
|
1356
|
+
var { logPath: restartLogPath, configPath: restartConfigPath } = require("./config");
|
|
1357
|
+
var daemonScript = path.join(__dirname, "daemon.js");
|
|
1358
|
+
var logFd = fs.openSync(restartLogPath(), "a");
|
|
1359
|
+
var child = spawnRestart(process.execPath, [daemonScript], {
|
|
1360
|
+
detached: true,
|
|
1361
|
+
windowsHide: true,
|
|
1362
|
+
stdio: ["ignore", logFd, logFd],
|
|
1363
|
+
env: Object.assign({}, process.env, {
|
|
1364
|
+
CLAY_CONFIG: restartConfigPath(),
|
|
1365
|
+
}),
|
|
1366
|
+
});
|
|
1367
|
+
child.unref();
|
|
1368
|
+
fs.closeSync(logFd);
|
|
1369
|
+
config.pid = child.pid;
|
|
1370
|
+
saveConfig(config);
|
|
1371
|
+
console.log("[daemon] Spawned new daemon (PID " + child.pid + "), exiting.");
|
|
1372
|
+
process.exit(120);
|
|
1373
|
+
} catch (e) {
|
|
1374
|
+
console.error("[daemon] Restart failed:", e.message);
|
|
1375
|
+
process.exit(1);
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1378
|
+
}).catch(function (e) {
|
|
1379
|
+
console.error("[daemon] Restart shutdown failed:", e && e.message ? e.message : e);
|
|
1380
|
+
if (relay.onboardingServer) relay.onboardingServer.close();
|
|
1381
|
+
relay.server.close(function () {
|
|
1332
1382
|
process.exit(1);
|
|
1333
|
-
}
|
|
1383
|
+
});
|
|
1334
1384
|
});
|
|
1335
1385
|
|
|
1336
|
-
// Force exit after
|
|
1386
|
+
// Force exit after 10 seconds if server.close hangs
|
|
1337
1387
|
setTimeout(function () {
|
|
1338
1388
|
console.error("[daemon] Forced exit after timeout during restart");
|
|
1339
1389
|
process.exit(1);
|
|
1340
|
-
},
|
|
1390
|
+
}, 10000);
|
|
1341
1391
|
} catch (e) {
|
|
1342
1392
|
console.error("[daemon] Restart failed:", e.message);
|
|
1343
1393
|
relay.broadcastAll({ type: "toast", level: "error", message: "Restart failed: " + e.message });
|
|
@@ -1347,9 +1397,12 @@ function spawnAndRestart() {
|
|
|
1347
1397
|
|
|
1348
1398
|
// --- Graceful shutdown ---
|
|
1349
1399
|
var updateHandoff = false; // true when shutting down for update (new daemon already spawned)
|
|
1400
|
+
var shutdownStarted = false;
|
|
1350
1401
|
|
|
1351
1402
|
function gracefulShutdown() {
|
|
1352
1403
|
try { console.log("[daemon] Shutting down..."); } catch (e) {}
|
|
1404
|
+
if (shutdownStarted) return;
|
|
1405
|
+
shutdownStarted = true;
|
|
1353
1406
|
var exitCode = updateHandoff ? 120 : 0; // 120 = update handoff, don't auto-restart
|
|
1354
1407
|
|
|
1355
1408
|
if (caffeinateProc) {
|
|
@@ -1369,22 +1422,30 @@ function gracefulShutdown() {
|
|
|
1369
1422
|
} catch (e) {}
|
|
1370
1423
|
}
|
|
1371
1424
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
}
|
|
1425
|
+
shutdownProjects().then(function () {
|
|
1426
|
+
if (relay.onboardingServer) {
|
|
1427
|
+
relay.onboardingServer.close();
|
|
1428
|
+
}
|
|
1377
1429
|
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1430
|
+
relay.server.close(function () {
|
|
1431
|
+
try { console.log("[daemon] Server closed"); } catch (e) {}
|
|
1432
|
+
process.exit(exitCode);
|
|
1433
|
+
});
|
|
1434
|
+
}).catch(function (e) {
|
|
1435
|
+
console.error("[daemon] Shutdown cleanup failed:", e && e.message ? e.message : e);
|
|
1436
|
+
if (relay.onboardingServer) {
|
|
1437
|
+
relay.onboardingServer.close();
|
|
1438
|
+
}
|
|
1439
|
+
relay.server.close(function () {
|
|
1440
|
+
process.exit(exitCode);
|
|
1441
|
+
});
|
|
1381
1442
|
});
|
|
1382
1443
|
|
|
1383
|
-
// Force exit after
|
|
1444
|
+
// Force exit after 10 seconds
|
|
1384
1445
|
setTimeout(function () {
|
|
1385
1446
|
try { console.error("[daemon] Forced exit after timeout"); } catch (e) {}
|
|
1386
1447
|
process.exit(1);
|
|
1387
|
-
},
|
|
1448
|
+
}, 10000);
|
|
1388
1449
|
}
|
|
1389
1450
|
|
|
1390
1451
|
process.on("SIGTERM", gracefulShutdown);
|