clay-server 2.34.0-beta.2 → 2.34.0-beta.4

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.
@@ -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/daemon.js CHANGED
@@ -904,6 +904,47 @@ var relay = createServer({
904
904
  },
905
905
  });
906
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
+
907
948
  // Worktree tracking extracted to daemon-projects.js
908
949
 
909
950
  // --- Register projects ---
@@ -1305,41 +1346,48 @@ function spawnAndRestart() {
1305
1346
  try {
1306
1347
  updateHandoff = true;
1307
1348
  ipc.close();
1308
- relay.destroyAll();
1309
- if (relay.onboardingServer) relay.onboardingServer.close();
1349
+ shutdownProjects().then(function () {
1350
+ if (relay.onboardingServer) relay.onboardingServer.close();
1310
1351
 
1311
- // Close the server first so the port is released before spawning the new daemon
1312
- relay.server.close(function () {
1313
- try {
1314
- var { spawn: spawnRestart } = require("child_process");
1315
- var { logPath: restartLogPath, configPath: restartConfigPath } = require("./config");
1316
- var daemonScript = path.join(__dirname, "daemon.js");
1317
- var logFd = fs.openSync(restartLogPath(), "a");
1318
- var child = spawnRestart(process.execPath, [daemonScript], {
1319
- detached: true,
1320
- windowsHide: true,
1321
- stdio: ["ignore", logFd, logFd],
1322
- env: Object.assign({}, process.env, {
1323
- CLAY_CONFIG: restartConfigPath(),
1324
- }),
1325
- });
1326
- child.unref();
1327
- fs.closeSync(logFd);
1328
- config.pid = child.pid;
1329
- saveConfig(config);
1330
- console.log("[daemon] Spawned new daemon (PID " + child.pid + "), exiting.");
1331
- process.exit(120);
1332
- } catch (e) {
1333
- console.error("[daemon] Restart failed:", e.message);
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 () {
1334
1382
  process.exit(1);
1335
- }
1383
+ });
1336
1384
  });
1337
1385
 
1338
- // Force exit after 5 seconds if server.close hangs
1386
+ // Force exit after 10 seconds if server.close hangs
1339
1387
  setTimeout(function () {
1340
1388
  console.error("[daemon] Forced exit after timeout during restart");
1341
1389
  process.exit(1);
1342
- }, 5000);
1390
+ }, 10000);
1343
1391
  } catch (e) {
1344
1392
  console.error("[daemon] Restart failed:", e.message);
1345
1393
  relay.broadcastAll({ type: "toast", level: "error", message: "Restart failed: " + e.message });
@@ -1349,9 +1397,12 @@ function spawnAndRestart() {
1349
1397
 
1350
1398
  // --- Graceful shutdown ---
1351
1399
  var updateHandoff = false; // true when shutting down for update (new daemon already spawned)
1400
+ var shutdownStarted = false;
1352
1401
 
1353
1402
  function gracefulShutdown() {
1354
1403
  try { console.log("[daemon] Shutting down..."); } catch (e) {}
1404
+ if (shutdownStarted) return;
1405
+ shutdownStarted = true;
1355
1406
  var exitCode = updateHandoff ? 120 : 0; // 120 = update handoff, don't auto-restart
1356
1407
 
1357
1408
  if (caffeinateProc) {
@@ -1371,22 +1422,30 @@ function gracefulShutdown() {
1371
1422
  } catch (e) {}
1372
1423
  }
1373
1424
 
1374
- relay.destroyAll();
1375
-
1376
- if (relay.onboardingServer) {
1377
- relay.onboardingServer.close();
1378
- }
1425
+ shutdownProjects().then(function () {
1426
+ if (relay.onboardingServer) {
1427
+ relay.onboardingServer.close();
1428
+ }
1379
1429
 
1380
- relay.server.close(function () {
1381
- try { console.log("[daemon] Server closed"); } catch (e) {}
1382
- process.exit(exitCode);
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
+ });
1383
1442
  });
1384
1443
 
1385
- // Force exit after 5 seconds
1444
+ // Force exit after 10 seconds
1386
1445
  setTimeout(function () {
1387
1446
  try { console.error("[daemon] Forced exit after timeout"); } catch (e) {}
1388
1447
  process.exit(1);
1389
- }, 5000);
1448
+ }, 10000);
1390
1449
  }
1391
1450
 
1392
1451
  process.on("SIGTERM", gracefulShutdown);
@@ -3,10 +3,12 @@ var path = require("path");
3
3
  var config = require("./config");
4
4
 
5
5
  var sqlite;
6
+ var _availabilityError = null;
6
7
  try {
7
8
  sqlite = require("node:sqlite");
8
9
  } catch (e) {
9
- throw new Error("Mate datastores require Node 22.13.0 or newer with node:sqlite available.");
10
+ sqlite = null;
11
+ _availabilityError = "Mate datastores require Node 22.13.0 or newer with node:sqlite available.";
10
12
  }
11
13
 
12
14
  function parseNodeVersion(version) {
@@ -24,10 +26,13 @@ function assertNodeVersion() {
24
26
  throw new Error("Mate datastores require Node 22.13.0 or newer.");
25
27
  }
26
28
  }
29
+ try {
30
+ assertNodeVersion();
31
+ } catch (e) {
32
+ if (!_availabilityError) _availabilityError = e.message;
33
+ }
27
34
 
28
- assertNodeVersion();
29
-
30
- var DatabaseSync = sqlite.DatabaseSync;
35
+ var DatabaseSync = sqlite ? sqlite.DatabaseSync : null;
31
36
 
32
37
  var MAX_ROWS = 200;
33
38
  var MAX_RESULT_BYTES = 1024 * 1024;
@@ -37,6 +42,20 @@ var CLAY_META_VERSION = "1";
37
42
 
38
43
  var _dbCache = {};
39
44
 
45
+ function isMateDatastoreAvailable() {
46
+ return !!DatabaseSync && !_availabilityError;
47
+ }
48
+
49
+ function getMateDatastoreAvailabilityError() {
50
+ return _availabilityError || null;
51
+ }
52
+
53
+ function assertAvailable() {
54
+ if (!isMateDatastoreAvailable()) {
55
+ throw new Error(getMateDatastoreAvailabilityError() || "Mate datastore is unavailable.");
56
+ }
57
+ }
58
+
40
59
  function getMateDbPath(opts) {
41
60
  if (opts && opts.dbPath) return String(opts.dbPath);
42
61
  if (opts && opts.mateDir) return path.join(String(opts.mateDir), "store.db");
@@ -77,6 +96,7 @@ function initClayMeta(db) {
77
96
  }
78
97
 
79
98
  function ensureMateDatastore(opts) {
99
+ assertAvailable();
80
100
  var dbPath = getMateDbPath(opts);
81
101
  if (_dbCache[dbPath] && _dbCache[dbPath].db) return _dbCache[dbPath];
82
102
  ensureParentDir(dbPath);
@@ -257,7 +277,7 @@ function listSchemaObjects(handle) {
257
277
  }
258
278
  try {
259
279
  var rows = wrapper.db.prepare(
260
- "SELECT name, type, sql FROM sqlite_master WHERE type IN ('table', 'view', 'index') AND name NOT LIKE 'sqlite_%' ORDER BY type, name"
280
+ "SELECT name, type, sql FROM sqlite_master WHERE type IN ('table', 'view', 'index') AND name NOT LIKE 'sqlite_%' AND name NOT LIKE 'clay_%' ORDER BY type, name"
261
281
  ).all();
262
282
  return { ok: true, objects: rows };
263
283
  } catch (e) {
@@ -325,6 +345,8 @@ function normalizeSqliteError(err, fallbackCode, fallbackMessage) {
325
345
  }
326
346
 
327
347
  module.exports = {
348
+ isMateDatastoreAvailable: isMateDatastoreAvailable,
349
+ getMateDatastoreAvailabilityError: getMateDatastoreAvailabilityError,
328
350
  openMateDatastore: openMateDatastore,
329
351
  ensureMateDatastore: ensureMateDatastore,
330
352
  listSchemaObjects: listSchemaObjects,
package/lib/mates.js CHANGED
@@ -131,6 +131,7 @@ function createMate(ctx, seedData) {
131
131
  createdBy: userId,
132
132
  createdAt: Date.now(),
133
133
  seedData: seedData || {},
134
+ vendor: (seedData && seedData.vendor) || "claude",
134
135
  profile: {
135
136
  displayName: null,
136
137
  avatarColor: colors[colorIdx],
@@ -158,7 +159,7 @@ function createMate(ctx, seedData) {
158
159
  yaml += "createdAt: " + mate.createdAt + "\n";
159
160
  yaml += "relationship: " + (seedData.relationship || "assistant") + "\n";
160
161
  yaml += "activities: " + JSON.stringify(seedData.activity || []) + "\n";
161
- yaml += "autonomy: " + (seedData.autonomy || "always_ask") + "\n";
162
+ yaml += "vendor: " + (seedData.vendor || "claude") + "\n";
162
163
  fs.writeFileSync(path.join(mateDir, "mate.yaml"), yaml);
163
164
 
164
165
  // Write initial CLAUDE.md (will be replaced by interview)
@@ -172,7 +173,6 @@ function createMate(ctx, seedData) {
172
173
  if (seedData.communicationStyle && seedData.communicationStyle.length > 0) {
173
174
  claudeMd += "- Communication: " + seedData.communicationStyle.join(", ") + "\n";
174
175
  }
175
- claudeMd += "- Autonomy: " + (seedData.autonomy || "always_ask") + "\n";
176
176
  var initialIdentity = claudeMd.trimEnd();
177
177
  claudeMd += TEAM_SECTION;
178
178
  claudeMd += SESSION_MEMORY_SECTION;
@@ -52,12 +52,27 @@ function attachConnection(ctx) {
52
52
  var getLatestVersion = ctx.getLatestVersion;
53
53
  var getTitle = ctx.getTitle;
54
54
  var getProject = ctx.getProject;
55
+ var warmup = ctx.warmup;
56
+
57
+ // Adapters are initialized lazily: the first websocket connection into
58
+ // this project triggers warmup. Without this guard we would either keep
59
+ // the old eager behavior (30+ Codex processes at daemon start) or run
60
+ // warmup once per reconnect.
61
+ var _warmedUp = false;
55
62
 
56
63
  function handleConnection(ws, wsUser, handleMessage, handleDisconnection) {
57
64
  ws._clayUser = wsUser || null;
58
65
  clients.add(ws);
59
66
  broadcastClientCount();
60
67
 
68
+ if (!_warmedUp) {
69
+ _warmedUp = true;
70
+ if (typeof warmup === "function") {
71
+ try { warmup(); }
72
+ catch (e) { console.error("[project-connection] warmup failed for " + slug + ":", e && e.message ? e.message : e); }
73
+ }
74
+ }
75
+
61
76
  var loopState = _loop.loopState;
62
77
  var loopRegistry = _loop.loopRegistry;
63
78
 
@@ -1,5 +1,6 @@
1
1
  var z = require("zod");
2
2
  var datastore = require("./mate-datastore");
3
+ var FEATURE_ENABLED = false;
3
4
 
4
5
  function attachMateDatastore(ctx) {
5
6
  var cwd = ctx.cwd;
@@ -7,7 +8,14 @@ function attachMateDatastore(ctx) {
7
8
  var send = ctx.send;
8
9
  var sendTo = ctx.sendTo;
9
10
 
11
+ function isFeatureAvailable() {
12
+ return FEATURE_ENABLED && datastore.isMateDatastoreAvailable();
13
+ }
14
+
10
15
  function ensureProjectDatastore() {
16
+ if (!FEATURE_ENABLED) {
17
+ return { ok: false, code: "MATE_DATASTORE_NOT_ALLOWED", message: "Mate datastore is not enabled." };
18
+ }
11
19
  if (!isMate) {
12
20
  return { ok: false, code: "MATE_DATASTORE_NOT_ALLOWED", message: "Mate datastore is only available in Mate sessions." };
13
21
  }
@@ -87,6 +95,10 @@ function attachMateDatastore(ctx) {
87
95
  return false;
88
96
  }
89
97
 
98
+ if (!FEATURE_ENABLED) {
99
+ return false;
100
+ }
101
+
90
102
  if (!isMate) {
91
103
  sendTo(ws, {
92
104
  type: "mate_db_error",
@@ -127,7 +139,7 @@ function attachMateDatastore(ctx) {
127
139
  }
128
140
 
129
141
  function getToolDefinitions() {
130
- if (!isMate) return [];
142
+ if (!isMate || !isFeatureAvailable()) return [];
131
143
  return [
132
144
  {
133
145
  name: "clay_db_query",
@@ -174,6 +186,7 @@ function attachMateDatastore(ctx) {
174
186
 
175
187
  function createMcpServer() {
176
188
  var defs = getToolDefinitions();
189
+ if (!defs.length) return null;
177
190
  var registered = {};
178
191
  for (var i = 0; i < defs.length; i++) {
179
192
  registered[defs[i].name] = {
@@ -192,7 +205,7 @@ function attachMateDatastore(ctx) {
192
205
  }
193
206
 
194
207
  function getSessionToolDefinitions() {
195
- if (!isMate) return null;
208
+ if (!isMate || !isFeatureAvailable()) return null;
196
209
  return getToolDefinitions();
197
210
  }
198
211
 
@@ -205,6 +218,7 @@ function attachMateDatastore(ctx) {
205
218
  }
206
219
 
207
220
  return {
221
+ isFeatureAvailable: isFeatureAvailable,
208
222
  handleMateDatastoreMessage: handleMateDatastoreMessage,
209
223
  getToolDefinitions: getToolDefinitions,
210
224
  createMcpServer: createMcpServer,
@@ -3,6 +3,33 @@ var path = require("path");
3
3
  var { execFileSync } = require("child_process");
4
4
  var { CODEX_DEFAULTS, getCodexConfig } = require("./codex-defaults");
5
5
 
6
+ // Format a user's answer to an ask_user_questions card as a plain user
7
+ // message so the MCP path can feed it back to the agent on the next turn.
8
+ // The agent sees: its own question text, then the selected answer(s).
9
+ function formatAskUserAnswerAsMessage(input, answers) {
10
+ var questions = (input && Array.isArray(input.questions)) ? input.questions : [];
11
+ if (questions.length === 0) {
12
+ // Shouldn't happen, but be defensive.
13
+ try { return "(answered with: " + JSON.stringify(answers || {}) + ")"; }
14
+ catch (e) { return "(answered)"; }
15
+ }
16
+ var lines = [];
17
+ for (var i = 0; i < questions.length; i++) {
18
+ var q = questions[i];
19
+ var qText = (q && q.question) ? q.question : ("Question " + (i + 1));
20
+ var ans = (answers && answers[i] != null) ? String(answers[i]) : "";
21
+ if (!ans) continue;
22
+ if (questions.length === 1) {
23
+ // Single question: keep it tight.
24
+ lines.push(ans);
25
+ } else {
26
+ lines.push("- " + qText + " → " + ans);
27
+ }
28
+ }
29
+ if (lines.length === 0) return "(no answer provided)";
30
+ return lines.join("\n");
31
+ }
32
+
6
33
  /**
7
34
  * Attach session management, config, project management, and mid-section
8
35
  * message handlers to a project context.
@@ -243,6 +270,17 @@ function attachSessions(ctx) {
243
270
 
244
271
  if (msg.type === "switch_session") {
245
272
  if (msg.id && sm.sessions.has(msg.id)) {
273
+ // If the target session's vendor doesn't own the currently cached
274
+ // model, clear sm.currentModel so the UI and next query don't leak
275
+ // the previous session's vendor-specific model into this one.
276
+ var switchTargetSess = sm.sessions.get(msg.id);
277
+ if (switchTargetSess && sm.currentModel) {
278
+ var targetVendor = switchTargetSess.vendor || sm.defaultVendor || null;
279
+ var tvModels = (targetVendor && sm.modelsByVendor && sm.modelsByVendor[targetVendor]) || [];
280
+ if (tvModels.length > 0 && tvModels.indexOf(sm.currentModel) === -1) {
281
+ sm.currentModel = "";
282
+ }
283
+ }
246
284
  // Check access in multi-user mode
247
285
  if (usersModule.isMultiUser() && ws._clayUser) {
248
286
  var switchTarget = sm.sessions.get(msg.id);
@@ -747,10 +785,41 @@ function attachSessions(ctx) {
747
785
  if (!pending) return true;
748
786
  delete session.pendingAskUser[toolId];
749
787
  sm.sendAndRecord(session, { type: "ask_user_answered", toolId: toolId, answers: answers });
750
- pending.resolve({
751
- behavior: "allow",
752
- updatedInput: Object.assign({}, pending.input, { answers: answers }),
753
- });
788
+
789
+ if (pending.mode === "mcp") {
790
+ // Stateless MCP path: the tool already returned. Inject the user's
791
+ // answer as a new user message so the conversation continues
792
+ // naturally on the next turn. This matches how the mate would see
793
+ // any other user input.
794
+ var answerText = formatAskUserAnswerAsMessage(pending.input, answers);
795
+ var userMsg = { type: "user_message", text: answerText };
796
+ session.history.push(userMsg);
797
+ sm.appendToSessionFile(session, userMsg);
798
+ sendToSession(session.localId, userMsg);
799
+
800
+ if (!session.isProcessing) {
801
+ session.isProcessing = true;
802
+ onProcessingChanged();
803
+ session.sentToolResults = {};
804
+ sendToSession(session.localId, { type: "status", status: "processing" });
805
+ if (!session.queryInstance && !session.worker) {
806
+ sdk.startQuery(session, answerText, undefined, getLinuxUserForSession(session));
807
+ } else {
808
+ sdk.pushMessage(session, answerText);
809
+ }
810
+ } else {
811
+ // Turn is still running; queue for the next turn.
812
+ sdk.pushMessage(session, answerText);
813
+ }
814
+ } else {
815
+ // Claude native AskUserQuestion path (canUseTool). The SDK is
816
+ // synchronously blocked on the permission callback, so we must
817
+ // resolve it with the standard permission shape.
818
+ pending.resolve({
819
+ behavior: "allow",
820
+ updatedInput: Object.assign({}, pending.input, { answers: answers }),
821
+ });
822
+ }
754
823
  return true;
755
824
  }
756
825