clay-server 2.47.0-beta.2 → 2.47.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.
@@ -43,7 +43,6 @@ var CLAY_MANAGED_ALLOW = [
43
43
  "mcp__clay-browser__browser_watch_tab",
44
44
  "mcp__clay-browser__browser_unwatch_tab",
45
45
  "mcp__clay-debate__propose_debate",
46
- "mcp__clay-datastore__*",
47
46
  "mcp__clay-history__*",
48
47
  // Email: read-side only. Send / reply / mark_read still prompt.
49
48
  "mcp__clay-email__clay_read_email",
@@ -1,12 +1,20 @@
1
1
  var CODEX_DEFAULTS = {
2
- approval: "on-failure",
2
+ approval: "on-request",
3
3
  sandbox: "danger-full-access",
4
4
  webSearch: "live",
5
5
  };
6
6
 
7
+ var CODEX_APPROVAL_POLICIES = ["untrusted", "on-request", "granular", "never"];
8
+
9
+ function normalizeCodexApproval(value) {
10
+ if (value === "on-failure") return CODEX_DEFAULTS.approval;
11
+ if (CODEX_APPROVAL_POLICIES.indexOf(value) === -1) return CODEX_DEFAULTS.approval;
12
+ return value;
13
+ }
14
+
7
15
  function getCodexConfig(sm) {
8
16
  return {
9
- approval: (sm && sm.codexApproval) || CODEX_DEFAULTS.approval,
17
+ approval: normalizeCodexApproval(sm && sm.codexApproval),
10
18
  sandbox: (sm && sm.codexSandbox) || CODEX_DEFAULTS.sandbox,
11
19
  webSearch: (sm && sm.codexWebSearch) || CODEX_DEFAULTS.webSearch,
12
20
  };
@@ -14,5 +22,7 @@ function getCodexConfig(sm) {
14
22
 
15
23
  module.exports = {
16
24
  CODEX_DEFAULTS: CODEX_DEFAULTS,
25
+ CODEX_APPROVAL_POLICIES: CODEX_APPROVAL_POLICIES,
26
+ normalizeCodexApproval: normalizeCodexApproval,
17
27
  getCodexConfig: getCodexConfig,
18
28
  };
@@ -4,6 +4,7 @@ var usersModule = require("./users");
4
4
  var userPresence = require("./user-presence");
5
5
  var emailAccounts = require("./email-accounts");
6
6
  var { getCodexConfig } = require("./codex-defaults");
7
+ var yoke = require("./yoke");
7
8
 
8
9
  /**
9
10
  * Attach connection/disconnection handlers to a project context.
@@ -132,7 +133,7 @@ function attachConnection(ctx) {
132
133
  var restoredActive = restoredState.active;
133
134
  var initialVendor = (restoredActive && restoredActive.vendor) || sm.defaultVendor || "claude";
134
135
  var initialModels = (sm.modelsByVendor && sm.modelsByVendor[initialVendor]) || sm.availableModels || [];
135
- sendTo(ws, { type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, dangerouslySkipPermissions: dangerouslySkipPermissions, osUsers: osUsers, lanHost: lanHost, projectCount: _filteredProjects.length, projects: _filteredProjects, projectOwnerId: projectOwnerId, ownerLocked: ownerLocked });
136
+ sendTo(ws, { type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, dangerouslySkipPermissions: dangerouslySkipPermissions, osUsers: osUsers, lanHost: lanHost, projectCount: _filteredProjects.length, projects: _filteredProjects, projectOwnerId: projectOwnerId, ownerLocked: ownerLocked, vendors: yoke.VENDOR_REGISTRY });
136
137
  // Update notifications are pushed on a scheduled interval (see
137
138
  // scheduleUpdateBroadcast). We no longer push on connect to avoid
138
139
  // re-triggering the banner on every page refresh.
@@ -5,6 +5,7 @@
5
5
  var fs = require("fs");
6
6
  var path = require("path");
7
7
  var config = require("./config");
8
+ var yoke = require("./yoke");
8
9
 
9
10
  var NOTIF_FILE = path.join(config.CONFIG_DIR, "notifications.json");
10
11
  var REMINDER_INTERVAL = 60 * 60 * 1000; // 1 hour
@@ -21,7 +22,8 @@ function generateId() {
21
22
  var formatters = {
22
23
  auth_required: function (data) {
23
24
  var vendor = data.vendor || "claude";
24
- var title = data.title || ((vendor === "codex" ? "Codex" : (vendor === "kiro" ? "Kiro CLI" : "Claude Code")) + " is not logged in");
25
+ var vendorInfo = yoke.getVendorInfo(vendor);
26
+ var title = data.title || (((vendorInfo && vendorInfo.displayName) || "Claude Code") + " is not logged in");
25
27
  return {
26
28
  type: "auth_required",
27
29
  title: title,
@@ -0,0 +1,366 @@
1
+ var yoke = require("./yoke");
2
+ var sessionSpawnMcp = require("./session-spawn-mcp-server");
3
+ var cliSessions = require("./cli-sessions");
4
+ var os = require("os");
5
+ var osUsers = require("./os-users");
6
+
7
+ var MAX_SESSIONS_PER_CALL = 10;
8
+ var MAX_CHILDREN_PER_PARENT = 20;
9
+ var SPAWN_CONCURRENCY = 3;
10
+
11
+ function parseBatch(raw) {
12
+ var parsed;
13
+ try {
14
+ parsed = JSON.parse(raw);
15
+ } catch (e) {
16
+ throw new Error("sessions must be a valid JSON array");
17
+ }
18
+ if (!Array.isArray(parsed)) {
19
+ throw new Error("sessions must be a valid JSON array");
20
+ }
21
+ if (parsed.length < 1 || parsed.length > MAX_SESSIONS_PER_CALL) {
22
+ throw new Error("sessions must contain between 1 and 10 entries");
23
+ }
24
+ var result = [];
25
+ for (var i = 0; i < parsed.length; i++) {
26
+ var entry = parsed[i];
27
+ if (!entry || typeof entry.prompt !== "string" || !entry.prompt.trim()) {
28
+ throw new Error("session " + (i + 1) + " must include a non-empty prompt");
29
+ }
30
+ var title = typeof entry.title === "string" ? entry.title.trim() : "";
31
+ result.push({
32
+ title: title || "Spawned task " + (i + 1),
33
+ prompt: entry.prompt.trim(),
34
+ });
35
+ }
36
+ return result;
37
+ }
38
+
39
+ function getSessionsArray(sessions) {
40
+ if (!sessions) return [];
41
+ if (typeof sessions.values === "function") return Array.from(sessions.values());
42
+ return sessions;
43
+ }
44
+
45
+ function assertSpawnAllowed(parent, sessions, requestedCount) {
46
+ if (!parent) throw new Error("no active parent session");
47
+ if (parent.spawn) throw new Error("spawned sessions cannot spawn further sessions");
48
+ var all = getSessionsArray(sessions);
49
+ var childCount = 0;
50
+ for (var i = 0; i < all.length; i++) {
51
+ if (all[i].spawn && all[i].spawn.parentId === parent.localId) childCount++;
52
+ }
53
+ if (childCount + requestedCount > MAX_CHILDREN_PER_PARENT) {
54
+ throw new Error("a parent session cannot have more than 20 children");
55
+ }
56
+ return childCount;
57
+ }
58
+
59
+ function validateVendor(vendor, adapters, linuxUser, getVendorInfo) {
60
+ if (!vendor || !adapters || !adapters[vendor]) {
61
+ throw new Error("vendor is not available: " + (vendor || "unknown"));
62
+ }
63
+ var lookup = getVendorInfo || yoke.getVendorInfo;
64
+ var info = lookup(vendor);
65
+ if (linuxUser && info && info.osUserIsolation === false) {
66
+ throw new Error((info.displayName || vendor) + " is not available for OS-isolated users");
67
+ }
68
+ return vendor;
69
+ }
70
+
71
+ function createSpawnQueue(concurrency) {
72
+ var limit = concurrency || SPAWN_CONCURRENCY;
73
+ var pending = [];
74
+ var running = [];
75
+
76
+ function startTask(task) {
77
+ task.state = "running";
78
+ running.push(task);
79
+ var finished = false;
80
+ function complete(err) {
81
+ if (finished) return;
82
+ finished = true;
83
+ var index = running.indexOf(task);
84
+ if (index !== -1) running.splice(index, 1);
85
+ task.state = err ? "error" : "done";
86
+ task.error = err || null;
87
+ pump();
88
+ }
89
+ try {
90
+ task.start(complete);
91
+ } catch (e) {
92
+ complete(e);
93
+ }
94
+ }
95
+
96
+ function pump() {
97
+ while (running.length < limit && pending.length > 0) {
98
+ var task = pending.shift();
99
+ startTask(task);
100
+ }
101
+ }
102
+
103
+ function add(tasks) {
104
+ for (var i = 0; i < tasks.length; i++) {
105
+ tasks[i].state = "queued";
106
+ pending.push(tasks[i]);
107
+ }
108
+ pump();
109
+ var queued = 0;
110
+ var active = 0;
111
+ for (var j = 0; j < tasks.length; j++) {
112
+ if (tasks[j].state === "queued") queued++;
113
+ if (tasks[j].state === "running") active++;
114
+ }
115
+ return { queued: queued, running: active };
116
+ }
117
+
118
+ return {
119
+ add: add,
120
+ get pendingCount() { return pending.length; },
121
+ get runningCount() { return running.length; },
122
+ };
123
+ }
124
+
125
+ function hasSessionError(session) {
126
+ var history = session.history || [];
127
+ var recent = history.slice(-3);
128
+ for (var i = 0; i < recent.length; i++) {
129
+ if (recent[i].type === "error" || (recent[i].type === "done" && recent[i].code === 1)) return true;
130
+ }
131
+ return false;
132
+ }
133
+
134
+ function toolResult(value) {
135
+ return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] });
136
+ }
137
+
138
+ function toolError(err) {
139
+ return Promise.resolve({
140
+ content: [{ type: "text", text: "Error: " + (err.message || err) }],
141
+ isError: true,
142
+ });
143
+ }
144
+
145
+ function rebuildMessageUUIDs(history) {
146
+ var messageUUIDs = [];
147
+ for (var i = 0; i < history.length; i++) {
148
+ if (history[i].type === "message_uuid") {
149
+ messageUUIDs.push({
150
+ uuid: history[i].uuid,
151
+ type: history[i].messageType,
152
+ historyIndex: i,
153
+ });
154
+ }
155
+ }
156
+ return messageUUIDs;
157
+ }
158
+
159
+ function attachSessionSpawn(ctx) {
160
+ var sm = ctx.sm;
161
+ var queue = createSpawnQueue(SPAWN_CONCURRENCY);
162
+ var adapters = ctx.adapters || {};
163
+ var readCliSessionHistory = ctx.readCliSessionHistory || cliSessions.readCliSessionHistory;
164
+
165
+ function resolveSessionHome(session) {
166
+ var linuxUser = ctx.getLinuxUserForSession(session);
167
+ if (linuxUser) {
168
+ try {
169
+ var info = osUsers.resolveOsUserInfo(linuxUser);
170
+ if (info && info.home) return info.home;
171
+ } catch (e) {}
172
+ }
173
+ return os.homedir();
174
+ }
175
+
176
+ function validateFork(parent, vendor, explicitVendor) {
177
+ if (!parent.cliSessionId) {
178
+ throw new Error("forkFromCurrent requires the calling session to have at least one completed turn");
179
+ }
180
+ var capabilities = sm.capabilitiesByVendor && sm.capabilitiesByVendor[vendor];
181
+ if (!capabilities || capabilities.fork !== true) {
182
+ throw new Error("forkFromCurrent is not supported by vendor: " + vendor);
183
+ }
184
+ var parentVendor = parent.vendor || sm.defaultVendor;
185
+ if (explicitVendor && explicitVendor !== parentVendor) {
186
+ throw new Error("forkFromCurrent children must use the parent's vendor");
187
+ }
188
+ }
189
+
190
+ function applyForkToTask(task, forkResult, history) {
191
+ task.session.cliSessionId = forkResult.sessionId;
192
+ task.session.history = history.slice();
193
+ task.session.messageUUIDs = rebuildMessageUUIDs(task.session.history);
194
+ sm.saveSessionFile(task.session);
195
+ }
196
+
197
+ function spawnOne(parent, spec, index, batchId, vendor, linuxUser) {
198
+ var create = typeof sm.createSessionRaw === "function" ? sm.createSessionRaw : sm.createSession;
199
+ var session = create.call(sm, {
200
+ ownerId: parent.ownerId || null,
201
+ sessionVisibility: parent.sessionVisibility || "shared",
202
+ vendor: vendor,
203
+ });
204
+ session.spawn = { parentId: parent.localId, index: index, batchId: batchId };
205
+ session.title = spec.title;
206
+ sm.saveSessionFile(session);
207
+
208
+ return {
209
+ session: session,
210
+ state: "created",
211
+ start: function (done) {
212
+ function finish(err) {
213
+ delete session.onQueryComplete;
214
+ delete session.singleTurn;
215
+ done(err);
216
+ }
217
+ var userMessage = { type: "user_message", text: spec.prompt };
218
+ session.history.push(userMessage);
219
+ sm.appendToSessionFile(session, userMessage);
220
+ session.isProcessing = true;
221
+ session.lastActivity = Date.now();
222
+ session.sentToolResults = {};
223
+ session.singleTurn = true;
224
+ session.onQueryComplete = function () {
225
+ finish(hasSessionError(session) ? new Error("child session failed") : null);
226
+ };
227
+ var sdk = ctx.getSdk();
228
+ if (!sdk || typeof sdk.startQuery !== "function") {
229
+ session.isProcessing = false;
230
+ finish(new Error("SDK bridge is not ready"));
231
+ return;
232
+ }
233
+ Promise.resolve(sdk.startQuery(session, spec.prompt, undefined, linuxUser)).then(function () {
234
+ if (!session.isProcessing && !session.queryInstance) {
235
+ finish(new Error("child session failed to start"));
236
+ }
237
+ }).catch(function (err) {
238
+ session.isProcessing = false;
239
+ session.history.push({ type: "error", text: err.message || String(err) });
240
+ finish(err);
241
+ });
242
+ },
243
+ };
244
+ }
245
+
246
+ async function spawn(args, caller) {
247
+ try {
248
+ // The caller is bound per query in getLocalMcpServers (project.js), so
249
+ // a child session resolving itself here is what makes the depth guard
250
+ // sound. Never fall back to sm.getActiveSession(): that is the
251
+ // project-global "last viewed" session and can belong to someone else.
252
+ var parent = caller;
253
+ if (!parent) throw new Error("spawn_sessions requires a session-bound tool server");
254
+ var specs = parseBatch(args.sessions);
255
+ assertSpawnAllowed(parent, sm.sessions, specs.length);
256
+ var explicitVendor = args.vendor && args.vendor.trim();
257
+ var vendor = explicitVendor || parent.vendor || sm.defaultVendor;
258
+ var linuxUser = ctx.getLinuxUserForSession(parent);
259
+ validateVendor(vendor, adapters, linuxUser);
260
+ var forkFromCurrent = args.forkFromCurrent === true;
261
+ if (forkFromCurrent) validateFork(parent, vendor, explicitVendor);
262
+ var batchId = "sp_" + Date.now().toString(36);
263
+ var tasks = [];
264
+ var spawned = [];
265
+ var failed = null;
266
+ var sdk = ctx.getSdk();
267
+ var lastMessage = parent.messageUUIDs && parent.messageUUIDs.length > 0
268
+ ? parent.messageUUIDs[parent.messageUUIDs.length - 1]
269
+ : null;
270
+ // Claude treats an omitted upToMessageId as a full-session fork;
271
+ // Codex forks the whole thread and ignores the UUID option.
272
+ var lastUuid = lastMessage ? lastMessage.uuid : undefined;
273
+ for (var i = 0; i < specs.length; i++) {
274
+ var forkResult = null;
275
+ var forkHistory = null;
276
+ if (forkFromCurrent) {
277
+ try {
278
+ if (!sdk || typeof sdk.forkSession !== "function") throw new Error("SDK bridge is not ready");
279
+ forkResult = await sdk.forkSession(parent, lastUuid);
280
+ if (!forkResult || !forkResult.sessionId) throw new Error("Fork returned no session id");
281
+ if (forkResult.useLocalHistory) {
282
+ forkHistory = parent.history.slice();
283
+ } else {
284
+ forkHistory = await readCliSessionHistory(
285
+ resolveSessionHome(parent), ctx.cwd, forkResult.sessionId
286
+ );
287
+ }
288
+ } catch (e) {
289
+ failed = { index: i, error: e.message || String(e) };
290
+ break;
291
+ }
292
+ }
293
+ var task = spawnOne(parent, specs[i], i, batchId, vendor, linuxUser);
294
+ if (forkResult) applyForkToTask(task, forkResult, forkHistory || []);
295
+ tasks.push(task);
296
+ spawned.push({ localId: task.session.localId, title: task.session.title });
297
+ }
298
+ var counts = queue.add(tasks);
299
+ if (tasks.length > 0) sm.broadcastSessionList();
300
+ var result = { spawned: spawned, queued: counts.queued, running: counts.running };
301
+ if (failed) result.failed = failed;
302
+ return toolResult(result);
303
+ } catch (e) {
304
+ return toolError(e);
305
+ }
306
+ }
307
+
308
+ function check(args, caller) {
309
+ try {
310
+ var parent = caller;
311
+ if (!parent) throw new Error("check_spawned_sessions requires a session-bound tool server");
312
+ var parentOnly = args.parentOnly !== false;
313
+ var all = getSessionsArray(sm.sessions);
314
+ var statuses = [];
315
+ for (var i = 0; i < all.length; i++) {
316
+ var session = all[i];
317
+ if (!session.spawn) continue;
318
+ if (parentOnly && session.spawn.parentId !== parent.localId) continue;
319
+ statuses.push({
320
+ localId: session.localId,
321
+ title: session.title || "New Session",
322
+ status: session.isProcessing ? "running" : (hasSessionError(session) ? "error" : "done"),
323
+ turnCount: session.turnCount || 0,
324
+ lastActivity: session.lastActivity || session.createdAt || 0,
325
+ });
326
+ }
327
+ return toolResult(statuses);
328
+ } catch (e) {
329
+ return toolError(e);
330
+ }
331
+ }
332
+
333
+ // boundSession: the session whose query this tool server instance is
334
+ // mounted into. Omitted for the static instance kept in the project's
335
+ // mcpServers map, which exists only so tool descriptors can be listed;
336
+ // its handlers fail closed if anything ever routes a call to them.
337
+ function createMcpServer(adapter, boundSession) {
338
+ if (ctx.isMate || !adapter || typeof adapter.createToolServer !== "function") return null;
339
+ return adapter.createToolServer({
340
+ name: "clay-sessions",
341
+ version: "1.0.0",
342
+ tools: sessionSpawnMcp.getToolDefs({
343
+ spawn: function (args) { return spawn(args, boundSession || null); },
344
+ check: function (args) { return check(args, boundSession || null); },
345
+ }),
346
+ });
347
+ }
348
+
349
+ return {
350
+ createMcpServer: createMcpServer,
351
+ spawnOne: spawnOne,
352
+ };
353
+ }
354
+
355
+ module.exports = {
356
+ MAX_SESSIONS_PER_CALL: MAX_SESSIONS_PER_CALL,
357
+ MAX_CHILDREN_PER_PARENT: MAX_CHILDREN_PER_PARENT,
358
+ SPAWN_CONCURRENCY: SPAWN_CONCURRENCY,
359
+ parseBatch: parseBatch,
360
+ assertSpawnAllowed: assertSpawnAllowed,
361
+ validateVendor: validateVendor,
362
+ createSpawnQueue: createSpawnQueue,
363
+ hasSessionError: hasSessionError,
364
+ rebuildMessageUUIDs: rebuildMessageUUIDs,
365
+ attachSessionSpawn: attachSessionSpawn,
366
+ };
@@ -3,6 +3,12 @@ var path = require("path");
3
3
  var crypto = require("crypto");
4
4
  var { execFileSync } = require("child_process");
5
5
  var { CODEX_DEFAULTS, getCodexConfig } = require("./codex-defaults");
6
+ var yoke = require("./yoke");
7
+
8
+ function vendorSupportsTui(vendor) {
9
+ var info = yoke.getVendorInfo(vendor);
10
+ return !!(info && info.sessionModes.indexOf("tui") !== -1);
11
+ }
6
12
 
7
13
  // Format a user's answer to an ask_user_questions card as a plain user
8
14
  // message so the MCP path can feed it back to the agent on the next turn.
@@ -274,6 +280,8 @@ function attachSessions(ctx) {
274
280
  var sid = session.cliSessionId;
275
281
  var localId = session.localId;
276
282
  var resumeSkip = session.dangerouslySkipPermissions ? " --dangerously-skip-permissions" : "";
283
+ // Command construction is Claude-specific. Generalize this before any
284
+ // other vendor declares "tui" in the YOKE registry.
277
285
  var cmd = "claude --resume " + sid + resumeSkip + "; exit\n";
278
286
  var term = tm.create(80, 24, getOsUserInfoForWs(ws), ws, {
279
287
  initialInput: cmd,
@@ -346,7 +354,7 @@ function attachSessions(ctx) {
346
354
  // born-TUI session shows the same read-only + Resume view as a fresh click.
347
355
  function resolveSessionForView(session, ws) {
348
356
  if (!session) return;
349
- if (session.vendor && session.vendor !== "claude") { session.tuiSuspended = false; return; }
357
+ if (session.vendor && !vendorSupportsTui(session.vendor)) { session.tuiSuspended = false; return; }
350
358
  var pref = getClaudeOpenModeForWs(ws);
351
359
  // A LIVE runtime always wins over the viewer's claudeOpenMode pref:
352
360
  // another user (or this user in another tab) may be in the session right
@@ -424,13 +432,13 @@ function attachSessions(ctx) {
424
432
  if (ws._clayUser && usersModule.isMultiUser()) sessionOpts.ownerId = ws._clayUser.id;
425
433
  if (msg.sessionVisibility) sessionOpts.sessionVisibility = msg.sessionVisibility;
426
434
  if (msg.vendor) sessionOpts.vendor = msg.vendor;
427
- // Mode resolution: codex and kiro sessions are always GUI (no TUI adapter).
428
- // Claude sessions honor the explicit msg.mode if provided, otherwise
435
+ // Mode resolution: vendors without a TUI session mode are always GUI.
436
+ // TUI-capable sessions honor the explicit msg.mode if provided, otherwise
429
437
  // fall back to the user's claudeOpenMode preference. This is what
430
438
  // makes the sidebar's "Claude" icon button create the right kind of
431
439
  // session without the client needing to know the preference.
432
440
  var requestedMode;
433
- if (msg.vendor === "codex" || msg.vendor === "kiro") {
441
+ if (msg.vendor && !vendorSupportsTui(msg.vendor)) {
434
442
  requestedMode = "gui";
435
443
  } else if (msg.mode === "tui" || msg.mode === "gui") {
436
444
  requestedMode = msg.mode;
@@ -625,7 +633,7 @@ function attachSessions(ctx) {
625
633
  // for every viewer; only cold sessions follow the clicker's
626
634
  // claudeOpenMode pref. Nothing is spawned here.
627
635
  var xmTarget = sm.sessions.get(msg.id);
628
- if (xmTarget && (xmTarget.vendor === "claude" || !xmTarget.vendor)) {
636
+ if (xmTarget && (!xmTarget.vendor || vendorSupportsTui(xmTarget.vendor))) {
629
637
  // Single source of truth: live runtime wins (tui stays tui, gui stays
630
638
  // gui); only cold sessions follow the viewer's pref. No PTY is spawned
631
639
  // on switch - born-TUI resumes lazily via the Resume bar
@@ -678,7 +686,7 @@ function attachSessions(ctx) {
678
686
  if (msg.type === "resume_tui_session") {
679
687
  if (msg.id && sm.sessions.has(msg.id)) {
680
688
  var rtTarget = sm.sessions.get(msg.id);
681
- var rtOk = rtTarget && (rtTarget.vendor === "claude" || !rtTarget.vendor) &&
689
+ var rtOk = rtTarget && (!rtTarget.vendor || vendorSupportsTui(rtTarget.vendor)) &&
682
690
  rtTarget.cliSessionId && tm;
683
691
  if (rtOk) {
684
692
  if (usersModule.isMultiUser() && ws._clayUser &&
@@ -706,7 +714,7 @@ function attachSessions(ctx) {
706
714
  if (msg.type === "suspend_tui_session") {
707
715
  if (msg.id && sm.sessions.has(msg.id)) {
708
716
  var stTarget = sm.sessions.get(msg.id);
709
- var stOk = stTarget && (stTarget.vendor === "claude" || !stTarget.vendor);
717
+ var stOk = stTarget && (!stTarget.vendor || vendorSupportsTui(stTarget.vendor));
710
718
  if (stOk && (!usersModule.isMultiUser() || !ws._clayUser ||
711
719
  usersModule.canAccessSession(ws._clayUser.id, stTarget, { visibility: "public" }))) {
712
720
  if (tm) {
@@ -737,7 +745,7 @@ function attachSessions(ctx) {
737
745
  var tprId = msg.id;
738
746
  var tprSess = (tprId && sm.sessions.has(tprId)) ? sm.sessions.get(tprId) : null;
739
747
  if (!tprSess || !tprSess.cliSessionId || tprSess.mode !== "tui") return true;
740
- if (tprSess.vendor && tprSess.vendor !== "claude") return true;
748
+ if (tprSess.vendor && !vendorSupportsTui(tprSess.vendor)) return true;
741
749
  if (usersModule.isMultiUser() && ws._clayUser
742
750
  && !usersModule.canAccessSession(ws._clayUser.id, tprSess, { visibility: "public" })) {
743
751
  return true;
@@ -1084,7 +1092,7 @@ function attachSessions(ctx) {
1084
1092
 
1085
1093
  // Codex-specific settings (stored on sessionManager, passed to adapter via adapterOptions)
1086
1094
  if (msg.type === "set_codex_approval") {
1087
- sm.codexApproval = msg.approval || CODEX_DEFAULTS.approval;
1095
+ sm.codexApproval = getCodexConfig({ codexApproval: msg.approval }).approval;
1088
1096
  send(Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
1089
1097
  return true;
1090
1098
  }