clay-server 2.31.0 → 2.32.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.
Files changed (82) hide show
  1. package/lib/browser-mcp-server.js +32 -44
  2. package/lib/codex-defaults.js +18 -0
  3. package/lib/debate-mcp-server.js +14 -31
  4. package/lib/mcp-local.js +31 -1
  5. package/lib/project-connection.js +9 -6
  6. package/lib/project-debate.js +8 -0
  7. package/lib/project-filesystem.js +47 -1
  8. package/lib/project-http.js +75 -8
  9. package/lib/project-mate-interaction.js +102 -16
  10. package/lib/project-mcp.js +4 -0
  11. package/lib/project-notifications.js +9 -0
  12. package/lib/project-sessions.js +94 -51
  13. package/lib/project-user-message.js +12 -7
  14. package/lib/project.js +234 -99
  15. package/lib/public/app.js +135 -454
  16. package/lib/public/codex-avatar.png +0 -0
  17. package/lib/public/css/debate.css +3 -2
  18. package/lib/public/css/filebrowser.css +91 -1
  19. package/lib/public/css/icon-strip.css +21 -5
  20. package/lib/public/css/input.css +338 -104
  21. package/lib/public/css/mates.css +43 -0
  22. package/lib/public/css/mention.css +48 -4
  23. package/lib/public/css/menus.css +1 -1
  24. package/lib/public/css/messages.css +2 -0
  25. package/lib/public/css/notifications-center.css +26 -0
  26. package/lib/public/css/tooltip.css +47 -0
  27. package/lib/public/index.html +78 -26
  28. package/lib/public/modules/app-connection.js +138 -37
  29. package/lib/public/modules/app-cursors.js +18 -17
  30. package/lib/public/modules/app-debate-ui.js +9 -9
  31. package/lib/public/modules/app-dm.js +175 -131
  32. package/lib/public/modules/app-favicon.js +28 -26
  33. package/lib/public/modules/app-header.js +79 -68
  34. package/lib/public/modules/app-home-hub.js +55 -47
  35. package/lib/public/modules/app-loop-ui.js +34 -18
  36. package/lib/public/modules/app-loop-wizard.js +6 -6
  37. package/lib/public/modules/app-messages.js +199 -153
  38. package/lib/public/modules/app-misc.js +23 -12
  39. package/lib/public/modules/app-notifications.js +119 -9
  40. package/lib/public/modules/app-panels.js +203 -49
  41. package/lib/public/modules/app-projects.js +161 -150
  42. package/lib/public/modules/app-rate-limit.js +5 -4
  43. package/lib/public/modules/app-rendering.js +149 -101
  44. package/lib/public/modules/app-skills-install.js +4 -4
  45. package/lib/public/modules/context-sources.js +102 -66
  46. package/lib/public/modules/dom-refs.js +21 -0
  47. package/lib/public/modules/filebrowser.js +173 -2
  48. package/lib/public/modules/input.js +122 -0
  49. package/lib/public/modules/markdown.js +5 -1
  50. package/lib/public/modules/mate-sidebar.js +38 -0
  51. package/lib/public/modules/mention.js +24 -6
  52. package/lib/public/modules/scheduler.js +1 -1
  53. package/lib/public/modules/sidebar-mates.js +79 -35
  54. package/lib/public/modules/sidebar-mobile.js +34 -30
  55. package/lib/public/modules/sidebar-projects.js +60 -57
  56. package/lib/public/modules/sidebar-sessions.js +75 -69
  57. package/lib/public/modules/sidebar.js +12 -20
  58. package/lib/public/modules/skills.js +8 -9
  59. package/lib/public/modules/sticky-notes.js +1 -2
  60. package/lib/public/modules/store.js +9 -2
  61. package/lib/public/modules/stt.js +4 -1
  62. package/lib/public/modules/terminal.js +12 -0
  63. package/lib/public/modules/tools.js +18 -13
  64. package/lib/public/modules/tooltip.js +32 -5
  65. package/lib/sdk-bridge.js +562 -1114
  66. package/lib/sdk-message-processor.js +150 -135
  67. package/lib/sdk-worker.js +4 -0
  68. package/lib/server-dm.js +1 -0
  69. package/lib/server.js +86 -1
  70. package/lib/sessions.js +81 -37
  71. package/lib/ws-schema.js +2 -0
  72. package/lib/yoke/adapters/claude-worker.js +559 -0
  73. package/lib/yoke/adapters/claude.js +1483 -0
  74. package/lib/yoke/adapters/codex.js +1121 -0
  75. package/lib/yoke/adapters/gemini.js +709 -0
  76. package/lib/yoke/codex-app-server.js +307 -0
  77. package/lib/yoke/index.js +199 -0
  78. package/lib/yoke/instructions.js +62 -0
  79. package/lib/yoke/interface.js +98 -0
  80. package/lib/yoke/mcp-bridge-server.js +294 -0
  81. package/lib/yoke/package.json +7 -0
  82. package/package.json +3 -1
@@ -4,6 +4,7 @@ var crypto = require("crypto");
4
4
 
5
5
  function attachMateInteraction(ctx) {
6
6
  var cwd = ctx.cwd;
7
+ var slug = ctx.slug || "";
7
8
  var sm = ctx.sm;
8
9
  var sdk = ctx.sdk;
9
10
  var send = ctx.send;
@@ -21,6 +22,7 @@ function attachMateInteraction(ctx) {
21
22
  var loadMateDigests = ctx.loadMateDigests;
22
23
  var updateMemorySummary = ctx.updateMemorySummary;
23
24
  var initMemorySummary = ctx.initMemorySummary;
25
+ var getNotificationsModule = ctx.getNotificationsModule || function () { return null; };
24
26
  // checkForDmDebateBrief is accessed via ctx at call time because
25
27
  // it comes from the debate module initialized after this one.
26
28
 
@@ -104,6 +106,30 @@ function attachMateInteraction(ctx) {
104
106
  return lines.join("\n") + "\n\n";
105
107
  }
106
108
 
109
+ function getHostMateId() {
110
+ if (typeof slug === "string" && slug.indexOf("mate-") === 0) {
111
+ return slug.substring(5) || null;
112
+ }
113
+ return null;
114
+ }
115
+
116
+ function notifyMentionResponse(session, mentionMateId, mentionMateName, fullText) {
117
+ var notificationsModule = getNotificationsModule();
118
+ var hostMateId = getHostMateId();
119
+ var preview = (fullText || "").replace(/\s+/g, " ").trim();
120
+ if (preview.length > 140) preview = preview.substring(0, 140) + "...";
121
+ if (!notificationsModule || !hostMateId) return;
122
+ notificationsModule.notify("mention_response", {
123
+ title: (mentionMateName || "Mate") + " responded",
124
+ preview: preview,
125
+ slug: slug,
126
+ sessionId: session.localId,
127
+ mateId: hostMateId,
128
+ avatarMateId: mentionMateId || null,
129
+ ownerId: session.ownerId || null,
130
+ });
131
+ }
132
+
107
133
  // --- Shared digest worker: one reusable Haiku session for gate+digest ---
108
134
  // Combines gate check and digest generation into a single prompt,
109
135
  // processes jobs sequentially from a queue, reuses the session across calls.
@@ -136,12 +162,19 @@ function attachMateInteraction(ctx) {
136
162
  } catch (e) {}
137
163
 
138
164
  // Combined gate + digest in one prompt (saves a full round-trip vs separate gate)
139
- var prompt = [
165
+ var promptParts = [
140
166
  "[SYSTEM: Memory Gate + Digest]",
141
167
  "You are a memory system for an AI Mate (role: " + (mateRole || "assistant") + ").",
142
168
  "",
143
- "Conversation (" + job.type + "):",
144
- job.conversationContent,
169
+ ];
170
+ if (job.priorSummary) {
171
+ promptParts.push("Prior conversation context (memory summary so far):");
172
+ promptParts.push(job.priorSummary);
173
+ promptParts.push("");
174
+ }
175
+ promptParts.push("Conversation (" + job.type + "):");
176
+ promptParts.push(job.conversationContent);
177
+ var prompt = promptParts.concat([
145
178
  "",
146
179
  "STEP 1: Should this be saved to memory?",
147
180
  'Answer "no" ONLY if the entire conversation is trivial (e.g. just "hi"/"hello").',
@@ -171,7 +204,7 @@ function attachMateInteraction(ctx) {
171
204
  "user_observations: OPTIONAL array. Include ONLY if you noticed meaningful patterns about the USER themselves (not the topic).",
172
205
  "Categories: pattern (repeated behavior 2+ times), decision (explicit choice with reasoning), reaction (emotional/attitude signal), preference (tool/style/communication preference).",
173
206
  "Omit the field entirely if nothing notable about the user.",
174
- ].join("\n");
207
+ ]).join("\n");
175
208
 
176
209
  function handleResult(text) {
177
210
  var cleaned = text.trim();
@@ -225,9 +258,14 @@ function attachMateInteraction(ctx) {
225
258
  }
226
259
  }
227
260
 
228
- updateMemorySummary(job.mateCtx, job.mateId, digestObj);
229
- maybeSynthesizeUserProfile(job.mateCtx, job.mateId);
230
- if (job.onDone) job.onDone();
261
+ // Skip summary update for failed parses -- the fallback digest would degrade quality
262
+ if (digestObj.topic !== "parse_failed") {
263
+ updateMemorySummary(job.mateCtx, job.mateId, digestObj);
264
+ maybeSynthesizeUserProfile(job.mateCtx, job.mateId);
265
+ if (job.onDone) job.onDone();
266
+ } else {
267
+ if (job.onError) job.onError(new Error("parse_failed"));
268
+ }
231
269
  processDigestQueue();
232
270
  }
233
271
 
@@ -249,7 +287,7 @@ function attachMateInteraction(ctx) {
249
287
  console.error("[digest-worker] Error:", err);
250
288
  _digestWorker = null;
251
289
  _digestWorkerTurns = 0;
252
- if (job.onDone) job.onDone();
290
+ if (job.onError) job.onError(err);
253
291
  processDigestQueue();
254
292
  },
255
293
  });
@@ -265,11 +303,11 @@ function attachMateInteraction(ctx) {
265
303
  onError: function (err) {
266
304
  console.error("[digest-worker] Create error:", err);
267
305
  _digestWorker = null;
268
- if (job.onDone) job.onDone();
306
+ if (job.onError) job.onError(err);
269
307
  processDigestQueue();
270
308
  },
271
- }).then(function (ws) { _digestWorker = ws; _digestWorkerTurns = 1; }).catch(function () {
272
- if (job.onDone) job.onDone();
309
+ }).then(function (ws) { _digestWorker = ws; _digestWorkerTurns = 1; }).catch(function (err) {
310
+ if (job.onError) job.onError(err || new Error("digest worker creation failed"));
273
311
  processDigestQueue();
274
312
  });
275
313
  }
@@ -303,10 +341,14 @@ function attachMateInteraction(ctx) {
303
341
  type: "mention",
304
342
  conversationContent: conversationContent,
305
343
  onDone: function () { mentionSession._digesting = false; },
344
+ onError: function () { mentionSession._digesting = false; },
306
345
  });
307
346
  }
308
347
 
309
- // Digest DM turn for mate projects - uses shared digest worker
348
+ // Digest DM turn for mate projects - uses shared digest worker.
349
+ // Delta-based: only collects new turns since the last successful digest.
350
+ // Concurrency debounce: turns that arrive while a digest is in-flight
351
+ // are naturally batched into the next flush.
310
352
  var _dmDigestPending = false;
311
353
  function digestDmTurn(session, responsePreview) {
312
354
  if (!isMate || _dmDigestPending) return;
@@ -314,11 +356,26 @@ function attachMateInteraction(ctx) {
314
356
  var mateCtx = matesModule.buildMateCtx(projectOwnerId);
315
357
  if (!matesModule.isMate(mateCtx, mateId)) return;
316
358
 
317
- // Collect full conversation from session history (all user + mate turns)
359
+ // Track digest index per session so switching sessions doesn't misalign.
360
+ // On resumed sessions (after restart), recover index from the last
361
+ // digest_checkpoint entry in history so undigested turns aren't lost.
362
+ if (typeof session._dmLastDigestedIndex !== "number") {
363
+ session._dmLastDigestedIndex = 0;
364
+ for (var ci = session.history.length - 1; ci >= 0; ci--) {
365
+ if (session.history[ci].type === "digest_checkpoint") {
366
+ session._dmLastDigestedIndex = session.history[ci].digestedIndex;
367
+ break;
368
+ }
369
+ }
370
+ }
371
+
372
+ // Collect only new turns since the last successful digest
318
373
  var conversationParts = [];
319
374
  var totalLen = 0;
320
375
  var CONV_CAP = 6000;
321
- for (var hi = 0; hi < session.history.length; hi++) {
376
+ var startIndex = session._dmLastDigestedIndex;
377
+ for (var hi = startIndex; hi < session.history.length; hi++) {
378
+ if (totalLen >= CONV_CAP) break;
322
379
  var entry = session.history[hi];
323
380
  if (entry.type === "user_message" && entry.text) {
324
381
  var uText = entry.text;
@@ -335,8 +392,8 @@ function attachMateInteraction(ctx) {
335
392
  conversationParts.push("Mate: " + aText);
336
393
  totalLen += aText.length;
337
394
  }
338
- if (totalLen >= CONV_CAP) break;
339
395
  }
396
+ // If the latest response hasn't landed in history yet, append it
340
397
  var lastResponseText = responsePreview || "";
341
398
  if (lastResponseText && conversationParts.length > 0) {
342
399
  var lastPart = conversationParts[conversationParts.length - 1];
@@ -362,14 +419,34 @@ function attachMateInteraction(ctx) {
362
419
  });
363
420
  }
364
421
 
422
+ // Read existing summary to give the digest worker context for delta content
423
+ var priorSummary = "";
424
+ try {
425
+ if (fs.existsSync(summaryFile)) {
426
+ priorSummary = fs.readFileSync(summaryFile, "utf8").trim();
427
+ }
428
+ } catch (e) {}
429
+
365
430
  _dmDigestPending = true;
431
+ var snapshotIndex = session.history.length;
366
432
 
367
433
  enqueueDigest({
368
434
  mateCtx: mateCtx,
369
435
  mateId: mateId,
370
436
  type: "dm",
437
+ priorSummary: priorSummary || "",
371
438
  conversationContent: conversationParts.join("\n"),
372
- onDone: function () { _dmDigestPending = false; },
439
+ onDone: function () {
440
+ session._dmLastDigestedIndex = snapshotIndex;
441
+ // Persist checkpoint so resumed sessions know where to continue
442
+ var checkpoint = { type: "digest_checkpoint", digestedIndex: snapshotIndex };
443
+ session.history.push(checkpoint);
444
+ sm.appendToSessionFile(session, checkpoint);
445
+ _dmDigestPending = false;
446
+ },
447
+ onError: function () {
448
+ _dmDigestPending = false;
449
+ },
373
450
  });
374
451
  }
375
452
 
@@ -445,6 +522,7 @@ function attachMateInteraction(ctx) {
445
522
  }
446
523
 
447
524
  session._mentionInProgress = true;
525
+ session._mentionActiveMateId = msg.mateId;
448
526
 
449
527
  // Send mention start indicator
450
528
  sendToSession(session.localId, {
@@ -478,6 +556,7 @@ function attachMateInteraction(ctx) {
478
556
  },
479
557
  onDone: function (fullText) {
480
558
  session._mentionInProgress = false;
559
+ session._mentionActiveMateId = null;
481
560
 
482
561
  // Save mention response to session history
483
562
  var mentionResponseEntry = {
@@ -504,6 +583,10 @@ function attachMateInteraction(ctx) {
504
583
  sendToSession(session.localId, { type: "mention_done", mateId: msg.mateId });
505
584
  send({ type: "mention_processing", mateId: msg.mateId, active: false });
506
585
 
586
+ if (isMate) {
587
+ notifyMentionResponse(session, msg.mateId, mateName, fullText);
588
+ }
589
+
507
590
  // Check if the mate wrote a debate brief during this turn
508
591
  ctx.checkForDmDebateBrief(session, msg.mateId, mateCtx);
509
592
 
@@ -512,6 +595,7 @@ function attachMateInteraction(ctx) {
512
595
  },
513
596
  onError: function (errMsg) {
514
597
  session._mentionInProgress = false;
598
+ session._mentionActiveMateId = null;
515
599
  // Clean up dead session
516
600
  if (session._mentionSessions && session._mentionSessions[msg.mateId]) {
517
601
  delete session._mentionSessions[msg.mateId];
@@ -560,6 +644,7 @@ function attachMateInteraction(ctx) {
560
644
 
561
645
  // Create new persistent mention session
562
646
  sdk.createMentionSession({
647
+ vendor: mate.vendor || null,
563
648
  claudeMd: claudeMd,
564
649
  initialContext: mentionContext,
565
650
  initialMessage: mentionFullInput,
@@ -614,6 +699,7 @@ function attachMateInteraction(ctx) {
614
699
  }
615
700
  }).catch(function (err) {
616
701
  session._mentionInProgress = false;
702
+ session._mentionActiveMateId = null;
617
703
  console.error("[mention] Failed to create session for mate " + msg.mateId + ":", err.message || err);
618
704
  sendToSession(session.localId, { type: "mention_error", mateId: msg.mateId, error: "Failed to create mention session." });
619
705
  });
@@ -73,6 +73,7 @@ function attachMcp(ctx) {
73
73
 
74
74
  function handleToolResult(msg) {
75
75
  var callId = msg.callId;
76
+ console.log("[mcp-bridge] Tool result received: " + callId);
76
77
  var pending = _pendingCalls[callId];
77
78
  if (!pending) return;
78
79
  if (pending.timer) clearTimeout(pending.timer);
@@ -82,6 +83,7 @@ function attachMcp(ctx) {
82
83
 
83
84
  function handleToolError(msg) {
84
85
  var callId = msg.callId;
86
+ console.log("[mcp-bridge] Tool error received: " + callId + " error=" + (msg.error || "unknown"));
85
87
  var pending = _pendingCalls[callId];
86
88
  if (!pending) return;
87
89
  if (pending.timer) clearTimeout(pending.timer);
@@ -223,12 +225,14 @@ function attachMcp(ctx) {
223
225
  var callId = "mc_" + Date.now() + "_" + crypto.randomUUID().slice(0, 8);
224
226
 
225
227
  var timer = setTimeout(function () {
228
+ console.log("[mcp-bridge] Tool call TIMEOUT: " + callId + " server=" + serverName + " tool=" + toolName);
226
229
  delete _pendingCalls[callId];
227
230
  reject(new Error("MCP tool call timed out after " + (TOOL_TIMEOUT_MS / 1000) + "s"));
228
231
  }, TOOL_TIMEOUT_MS);
229
232
 
230
233
  _pendingCalls[callId] = { resolve: resolve, reject: reject, timer: timer };
231
234
 
235
+ console.log("[mcp-bridge] Sending tool call: " + callId + " server=" + serverName + " tool=" + toolName);
232
236
  sendTo(extWs, {
233
237
  type: "mcp_tool_call",
234
238
  callId: callId,
@@ -60,6 +60,15 @@ var formatters = {
60
60
  body: data.preview || "Sent you a message",
61
61
  };
62
62
  },
63
+
64
+ mention_response: function (data) {
65
+ return {
66
+ type: "mention_response",
67
+ title: data.title || "Mention response ready",
68
+ body: data.preview || "",
69
+ meta: { avatarMateId: data.avatarMateId || null },
70
+ };
71
+ },
63
72
  };
64
73
 
65
74
  // ========================================================
@@ -1,6 +1,7 @@
1
1
  var fs = require("fs");
2
2
  var path = require("path");
3
3
  var { execFileSync } = require("child_process");
4
+ var { CODEX_DEFAULTS, getCodexConfig } = require("./codex-defaults");
4
5
 
5
6
  /**
6
7
  * Attach session management, config, project management, and mid-section
@@ -13,7 +14,7 @@ var { execFileSync } = require("child_process");
13
14
  * opts, usersModule, userPresence, matesModule, pushModule,
14
15
  * getSessionForWs, getLinuxUserForSession, getOsUserInfoForWs,
15
16
  * hydrateImageRefs, onProcessingChanged, broadcastPresence,
16
- * getSDK, getProjectList, getProjectCount, getScheduleCount,
17
+ * adapter, getProjectList, getProjectCount, getScheduleCount,
17
18
  * moveScheduleToProject, moveAllSchedulesToProject, getHubSchedules,
18
19
  * fetchVersion, isNewer, onCreateWorktree, IGNORED_DIRS,
19
20
  * scheduleMessage, cancelScheduledMessage,
@@ -46,7 +47,7 @@ function attachSessions(ctx) {
46
47
  var hydrateImageRefs = ctx.hydrateImageRefs;
47
48
  var onProcessingChanged = ctx.onProcessingChanged;
48
49
  var broadcastPresence = ctx.broadcastPresence;
49
- var getSDK = ctx.getSDK;
50
+ var adapter = ctx.adapter;
50
51
  var getProjectList = ctx.getProjectList;
51
52
  var getProjectCount = ctx.getProjectCount;
52
53
  var getScheduleCount = ctx.getScheduleCount;
@@ -96,6 +97,7 @@ function attachSessions(ctx) {
96
97
  var sessionOpts = {};
97
98
  if (ws._clayUser && usersModule.isMultiUser()) sessionOpts.ownerId = ws._clayUser.id;
98
99
  if (msg.sessionVisibility) sessionOpts.sessionVisibility = msg.sessionVisibility;
100
+ if (msg.vendor) sessionOpts.vendor = msg.vendor;
99
101
  var newSess = sm.createSession(sessionOpts, ws);
100
102
  ws._clayActiveSession = newSess.localId;
101
103
  // Apply project-level email defaults to new session
@@ -153,9 +155,7 @@ function attachSessions(ctx) {
153
155
  if (!msg.cliSessionId) return true;
154
156
  var cliSess = require("./cli-sessions");
155
157
  // Try SDK for title first, then fall back to manual parsing
156
- var titlePromise = getSDK().then(function(sdkMod) {
157
- return sdkMod.getSessionInfo(msg.cliSessionId, { dir: cwd });
158
- }).then(function(info) {
158
+ var titlePromise = adapter.getSessionInfo(msg.cliSessionId, { dir: cwd }).then(function(info) {
159
159
  return (info && info.summary) ? info.summary.substring(0, 100) : null;
160
160
  }).catch(function() { return null; });
161
161
 
@@ -200,9 +200,7 @@ function attachSessions(ctx) {
200
200
  }
201
201
  } catch (e) {}
202
202
 
203
- getSDK().then(function(sdkMod) {
204
- return sdkMod.listSessions({ dir: cwd });
205
- }).then(function(sdkSessions) {
203
+ adapter.listSessions({ dir: cwd }).then(function(sdkSessions) {
206
204
  var filtered = sdkSessions.filter(function(s) {
207
205
  return !relayIds[s.sessionId];
208
206
  }).map(function(s) {
@@ -283,15 +281,14 @@ function attachSessions(ctx) {
283
281
  if (msg.id && sm.sessions.has(msg.id) && msg.title) {
284
282
  var s = sm.sessions.get(msg.id);
285
283
  s.title = String(msg.title).substring(0, 100);
284
+ s.titleManuallySet = true;
286
285
  sm.saveSessionFile(s);
287
286
  sm.broadcastSessionList();
288
287
  // Sync title to SDK session
289
288
  if (s.cliSessionId) {
290
- getSDK().then(function(sdkInst) {
291
- sdkInst.renameSession(s.cliSessionId, s.title, { dir: cwd }).catch(function(e) {
292
- console.error("[project] SDK renameSession failed:", e.message);
293
- });
294
- }).catch(function() {});
289
+ adapter.renameSession(s.cliSessionId, s.title, { dir: cwd }).catch(function(e) {
290
+ console.error("[project] SDK renameSession failed:", e.message);
291
+ });
295
292
  }
296
293
  }
297
294
  return true;
@@ -381,8 +378,9 @@ function attachSessions(ctx) {
381
378
 
382
379
  if (msg.type === "stop") {
383
380
  var session = getSessionForWs(ws);
384
- if (session && session.abortController && session.isProcessing) {
385
- session.abortController.abort();
381
+ if (session && session.isProcessing) {
382
+ session.taskStopRequested = true;
383
+ if (session.abortController) session.abortController.abort();
386
384
  }
387
385
  return true;
388
386
  }
@@ -521,32 +519,34 @@ function attachSessions(ctx) {
521
519
  return true;
522
520
  }
523
521
 
522
+ // Codex-specific settings (stored on sessionManager, passed to adapter via adapterOptions)
523
+ if (msg.type === "set_codex_approval") {
524
+ sm.codexApproval = msg.approval || CODEX_DEFAULTS.approval;
525
+ send(Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
526
+ return true;
527
+ }
528
+ if (msg.type === "set_codex_sandbox") {
529
+ sm.codexSandbox = msg.sandbox || CODEX_DEFAULTS.sandbox;
530
+ send(Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
531
+ return true;
532
+ }
533
+ if (msg.type === "set_codex_websearch") {
534
+ sm.codexWebSearch = msg.webSearch || CODEX_DEFAULTS.webSearch;
535
+ send(Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
536
+ return true;
537
+ }
538
+
524
539
  if (msg.type === "rewind_preview") {
525
540
  var session = getSessionForWs(ws);
526
541
  if (!session || !session.cliSessionId || !msg.uuid) return true;
527
- // Reject preview requests while a rewind is executing
528
542
  if (session._rewindInProgress) return true;
529
543
 
530
544
  (async function () {
531
- var result;
532
545
  try {
533
- result = await sdk.getOrCreateRewindQuery(session);
534
- var preview = await result.query.rewindFiles(msg.uuid, { dryRun: true });
535
- var diffs = {};
536
- var changedFiles = preview.filesChanged || [];
537
- for (var f = 0; f < changedFiles.length; f++) {
538
- try {
539
- diffs[changedFiles[f]] = execFileSync(
540
- "git", ["diff", "HEAD", "--", changedFiles[f]],
541
- { cwd: cwd, encoding: "utf8", timeout: 5000 }
542
- ) || "";
543
- } catch (e) { diffs[changedFiles[f]] = ""; }
544
- }
545
- sendTo(ws, { type: "rewind_preview_result", preview: preview, diffs: diffs, uuid: msg.uuid });
546
+ var r = await sdk.rewindPreview(session, msg.uuid);
547
+ sendTo(ws, { type: "rewind_preview_result", preview: r.preview, diffs: r.diffs, uuid: msg.uuid, chatOnly: r.chatOnly || false });
546
548
  } catch (err) {
547
549
  sendTo(ws, { type: "rewind_error", text: "Failed to preview rewind: " + err.message });
548
- } finally {
549
- if (result && result.isTemp) result.cleanup();
550
550
  }
551
551
  })();
552
552
  return true;
@@ -564,12 +564,10 @@ function attachSessions(ctx) {
564
564
  var mode = msg.mode || "both";
565
565
 
566
566
  (async function () {
567
- var result;
568
567
  try {
569
- // File restoration (skip for chat-only mode)
568
+ // File restoration (delegated to adapter via sdk-bridge)
570
569
  if (mode !== "chat") {
571
- result = await sdk.getOrCreateRewindQuery(session);
572
- await result.query.rewindFiles(msg.uuid, { dryRun: false });
570
+ await sdk.rewindExecuteFiles(session, msg.uuid);
573
571
  }
574
572
 
575
573
  // Conversation rollback (skip for files-only mode)
@@ -582,6 +580,14 @@ function attachSessions(ctx) {
582
580
  }
583
581
  }
584
582
 
583
+ // Count turns to roll back BEFORE trimming local history
584
+ var turnsToRollBack = 0;
585
+ if (targetIdx >= 0) {
586
+ for (var ri = targetIdx; ri < session.messageUUIDs.length; ri++) {
587
+ if (session.messageUUIDs[ri].type === "user") turnsToRollBack++;
588
+ }
589
+ }
590
+
585
591
  if (targetIdx >= 0) {
586
592
  var trimTo = session.messageUUIDs[targetIdx].historyIndex;
587
593
  for (var k = trimTo - 1; k >= 0; k--) {
@@ -592,6 +598,19 @@ function attachSessions(ctx) {
592
598
  }
593
599
  session.history = session.history.slice(0, trimTo);
594
600
  session.messageUUIDs = session.messageUUIDs.slice(0, targetIdx);
601
+ // Reset digest checkpoint if it points past the trimmed history
602
+ if (typeof session._dmLastDigestedIndex === "number" && session._dmLastDigestedIndex > trimTo) {
603
+ session._dmLastDigestedIndex = trimTo;
604
+ }
605
+ }
606
+
607
+ // Notify adapter of conversation rollback (e.g. Codex thread/rollback)
608
+ if (turnsToRollBack > 0) {
609
+ try {
610
+ await sdk.rollbackConversation(session, turnsToRollBack);
611
+ } catch (rbErr) {
612
+ console.error("[project-sessions] conversation rollback failed:", rbErr.message || rbErr);
613
+ }
595
614
  }
596
615
 
597
616
  var kept = session.messageUUIDs;
@@ -622,7 +641,6 @@ function attachSessions(ctx) {
622
641
  sendTo(ws, { type: "rewind_error", text: "Rewind failed: " + err.message });
623
642
  } finally {
624
643
  session._rewindInProgress = false;
625
- if (result && result.isTemp) result.cleanup();
626
644
  }
627
645
  })();
628
646
  return true;
@@ -634,22 +652,47 @@ function attachSessions(ctx) {
634
652
  sendTo(ws, { type: "error", text: "Cannot fork: no CLI session" });
635
653
  return true;
636
654
  }
637
- var forkCliId = session.cliSessionId;
638
655
  var forkTitle = (session.title || "New Session") + " (fork)";
639
- getSDK().then(function(sdkMod) {
640
- return sdkMod.forkSession(forkCliId, {
641
- upToMessageId: msg.uuid,
642
- dir: cwd,
643
- });
644
- }).then(function(result) {
645
- var cliSess = require("./cli-sessions");
646
- return cliSess.readCliSessionHistory(cwd, result.sessionId).then(function(history) {
647
- var forked = sm.resumeSession(result.sessionId, { history: history, title: forkTitle }, ws);
648
- if (forked) {
649
- ws._clayActiveSession = forked.localId;
650
- sendTo(ws, { type: "fork_complete", sessionId: forked.localId });
656
+
657
+ sdk.forkSession(session, msg.uuid).then(function(result) {
658
+ if (result.useLocalHistory) {
659
+ // Copy local history up to the target UUID
660
+ var targetIdx = -1;
661
+ for (var fi = 0; fi < session.messageUUIDs.length; fi++) {
662
+ if (session.messageUUIDs[fi].uuid === msg.uuid) { targetIdx = fi; break; }
651
663
  }
652
- });
664
+ var forkHistory = [];
665
+ if (targetIdx >= 0) {
666
+ var trimTo = session.messageUUIDs[targetIdx].historyIndex;
667
+ forkHistory = session.history.slice(0, trimTo);
668
+ } else {
669
+ forkHistory = session.history.slice();
670
+ }
671
+ var forked = sm.createSession({ vendor: session.vendor, ownerId: session.ownerId || null }, ws);
672
+ forked.cliSessionId = result.sessionId;
673
+ forked.title = forkTitle;
674
+ forked.history = forkHistory;
675
+ forked.messageUUIDs = [];
676
+ for (var hi = 0; hi < forkHistory.length; hi++) {
677
+ if (forkHistory[hi].type === "message_uuid") {
678
+ forked.messageUUIDs.push({ uuid: forkHistory[hi].uuid, type: forkHistory[hi].messageType, historyIndex: hi });
679
+ }
680
+ }
681
+ sm.saveSessionFile(forked);
682
+ sm.switchSession(forked.localId, ws, hydrateImageRefs);
683
+ sendTo(ws, { type: "fork_complete", sessionId: forked.localId });
684
+ sm.broadcastSessionList();
685
+ } else {
686
+ // Read history from CLI session files
687
+ var cliSess = require("./cli-sessions");
688
+ return cliSess.readCliSessionHistory(cwd, result.sessionId).then(function(history) {
689
+ var forked = sm.resumeSession(result.sessionId, { history: history, title: forkTitle }, ws);
690
+ if (forked) {
691
+ ws._clayActiveSession = forked.localId;
692
+ sendTo(ws, { type: "fork_complete", sessionId: forked.localId });
693
+ }
694
+ });
695
+ }
653
696
  }).catch(function(e) {
654
697
  sendTo(ws, { type: "error", text: "Fork failed: " + (e.message || e) });
655
698
  });
@@ -23,7 +23,7 @@ var fs = require("fs");
23
23
  * scheduleMessage, cancelScheduledMessage,
24
24
  * loadContextSources, saveContextSources,
25
25
  * digestDmTurn, gateMemory,
26
- * getSDK - lazy ESM loader returning Promise<sdk>
26
+ * adapter - YOKE adapter instance
27
27
  */
28
28
  function attachUserMessage(ctx) {
29
29
  var cwd = ctx.cwd;
@@ -69,7 +69,7 @@ function attachUserMessage(ctx) {
69
69
  var loadContextSources = ctx.loadContextSources;
70
70
  var saveContextSources = ctx.saveContextSources;
71
71
 
72
- var getSDK = ctx.getSDK;
72
+ var adapter = ctx.adapter;
73
73
  var _email = ctx._email;
74
74
 
75
75
  // --------------- Sticky notes ---------------
@@ -292,6 +292,13 @@ function attachUserMessage(ctx) {
292
292
  var session = getSessionForWs(ws);
293
293
  if (!session) return true;
294
294
 
295
+ // Bind vendor to session on first message (if not already set)
296
+ if (!session.vendor && msg.vendor) {
297
+ session.vendor = msg.vendor;
298
+ sm.saveSessionFile(session);
299
+ sm.broadcastSessionList();
300
+ }
301
+
295
302
  // Backfill ownerId for legacy sessions restored without one (multi-user only)
296
303
  if (!session.ownerId && ws._clayUser && usersModule.isMultiUser()) {
297
304
  session.ownerId = ws._clayUser.id;
@@ -337,11 +344,9 @@ function attachUserMessage(ctx) {
337
344
  sm.broadcastSessionList();
338
345
  // Sync auto-title to SDK
339
346
  if (session.cliSessionId) {
340
- getSDK().then(function(sdkMod) {
341
- sdkMod.renameSession(session.cliSessionId, session.title, { dir: cwd }).catch(function(e) {
342
- console.error("[project] SDK renameSession failed:", e.message);
343
- });
344
- }).catch(function() {});
347
+ adapter.renameSession(session.cliSessionId, session.title, { dir: cwd }).catch(function(e) {
348
+ console.error("[project] SDK renameSession failed:", e.message);
349
+ });
345
350
  }
346
351
  }
347
352