clay-server 5.2.0-beta.4 → 5.2.0-beta.5

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 (171) hide show
  1. package/lib/account-auth-socket.js +1 -0
  2. package/lib/builtin-mates.js +17 -14
  3. package/lib/canonical-clay-record-authority.js +15 -0
  4. package/lib/canonical-clay-scheduling-authority.js +19 -0
  5. package/lib/claude-hook-installer.js +28 -0
  6. package/lib/clay-mate-chat-receipts.js +26 -0
  7. package/lib/clay-mate-chat.js +87 -0
  8. package/lib/clay-mate-knowledge-input.js +26 -0
  9. package/lib/clay-mate-knowledge.js +95 -0
  10. package/lib/clay-mate-lifecycle.js +95 -0
  11. package/lib/clay-product-control-input.js +33 -0
  12. package/lib/clay-product-control-tools.js +51 -0
  13. package/lib/clay-product-control.js +237 -0
  14. package/lib/clay-project-lifecycle.js +154 -0
  15. package/lib/clay-project-record-input.js +44 -0
  16. package/lib/clay-project-records.js +83 -0
  17. package/lib/clay-scheduling-input.js +21 -0
  18. package/lib/clay-scheduling-receipts.js +26 -0
  19. package/lib/clay-scheduling.js +119 -0
  20. package/lib/clay-screen-controls.js +96 -0
  21. package/lib/clay-workspace-controls.js +83 -0
  22. package/lib/clay-workspace-input.js +17 -0
  23. package/lib/clay-workspace-receipts.js +26 -0
  24. package/lib/daemon-project-access.js +11 -0
  25. package/lib/daemon-project-lifecycle-callbacks.js +15 -0
  26. package/lib/daemon-project-lifecycle.js +262 -0
  27. package/lib/daemon-projects.js +1 -1
  28. package/lib/daemon.js +73 -221
  29. package/lib/issues-service.js +29 -15
  30. package/lib/knowledge-db-import.js +5 -4
  31. package/lib/knowledge-db-service.js +9 -6
  32. package/lib/knowledge-document-index.js +2 -1
  33. package/lib/knowledge-product-operations.js +46 -0
  34. package/lib/mate-creation-mcp-server.js +2 -0
  35. package/lib/mate-knowledge-canonical-access.js +10 -0
  36. package/lib/mate-knowledge-service.js +7 -4
  37. package/lib/mate-lifecycle-service.js +94 -0
  38. package/lib/mate-message-delivery.js +43 -0
  39. package/lib/mate-ready-creation.js +9 -1
  40. package/lib/mate-schedule-input.js +29 -0
  41. package/lib/mate-schedule-lifecycle.js +151 -0
  42. package/lib/mate-scheduling.js +103 -0
  43. package/lib/mates.js +4 -3
  44. package/lib/notes-checklist.js +50 -0
  45. package/lib/notes.js +49 -3
  46. package/lib/os-users.js +20 -5
  47. package/lib/project-admin-overview.js +51 -0
  48. package/lib/project-ask-user.js +4 -2
  49. package/lib/project-delegated-follow-up.js +2 -1
  50. package/lib/project-directory-claim.js +65 -0
  51. package/lib/project-file-operations.js +61 -0
  52. package/lib/project-filesystem.js +13 -87
  53. package/lib/project-full-access.js +23 -2
  54. package/lib/project-knowledge.js +32 -18
  55. package/lib/project-lifecycle-operations.js +98 -0
  56. package/lib/project-lifecycle-process-witness.js +32 -0
  57. package/lib/project-lifecycle-runtime.js +50 -0
  58. package/lib/project-linux-access.js +22 -0
  59. package/lib/project-logs-canonical-clay.js +27 -0
  60. package/lib/project-logs-clay-query.js +16 -0
  61. package/lib/project-logs-schema.js +4 -2
  62. package/lib/project-logs-service.js +8 -7
  63. package/lib/project-logs.js +1 -0
  64. package/lib/project-loop.js +1 -0
  65. package/lib/project-mate-chat-control.js +55 -0
  66. package/lib/project-mate-creation-proposal.js +18 -3
  67. package/lib/project-models.js +14 -31
  68. package/lib/project-notifications.js +74 -16
  69. package/lib/project-ownership.js +77 -0
  70. package/lib/project-pair-result-delivery.js +14 -3
  71. package/lib/project-pending-message-queue.js +1 -0
  72. package/lib/project-product-control.js +87 -0
  73. package/lib/project-product-session-queue.js +46 -0
  74. package/lib/project-push-access.js +28 -0
  75. package/lib/project-scheduled-message-queue.js +30 -0
  76. package/lib/project-scheduled-messages.js +63 -64
  77. package/lib/project-scheduled-task-control.js +83 -0
  78. package/lib/project-scheduled-tasks.js +13 -2
  79. package/lib/project-scheduling-access.js +10 -0
  80. package/lib/project-session-actions.js +65 -0
  81. package/lib/project-session-delete.js +8 -1
  82. package/lib/project-session-github.js +47 -12
  83. package/lib/project-session-history-actions.js +165 -0
  84. package/lib/project-session-runtime-actions.js +63 -0
  85. package/lib/project-session-vendor.js +54 -0
  86. package/lib/project-sessions.js +64 -241
  87. package/lib/project-user-message.js +30 -1
  88. package/lib/project-workspace-operations.js +126 -0
  89. package/lib/project-workspace-query.js +9 -2
  90. package/lib/project.js +95 -3
  91. package/lib/public/css/clay-tool-activity.css +51 -0
  92. package/lib/public/css/home-mate-creation.css +5 -0
  93. package/lib/public/css/overlays.css +4 -2
  94. package/lib/public/css/project-admin-overview.css +13 -0
  95. package/lib/public/css/session-folders.css +11 -0
  96. package/lib/public/css/session-github.css +3 -0
  97. package/lib/public/css/sticky-notes.css +61 -1
  98. package/lib/public/index.html +1 -1
  99. package/lib/public/modules/admin-settings-projects.js +6 -2
  100. package/lib/public/modules/app-connection.js +8 -1
  101. package/lib/public/modules/app-message-router.js +1 -1
  102. package/lib/public/modules/app-messages.js +18 -3
  103. package/lib/public/modules/app-projects.js +5 -0
  104. package/lib/public/modules/clay-owner-navigation.js +27 -0
  105. package/lib/public/modules/clay-tool-activity.js +140 -0
  106. package/lib/public/modules/clay-tool-presentation.js +338 -0
  107. package/lib/public/modules/command-palette.js +1 -1
  108. package/lib/public/modules/default-ai.js +11 -3
  109. package/lib/public/modules/home-debate-planning.js +5 -5
  110. package/lib/public/modules/home-mate-chat.js +8 -6
  111. package/lib/public/modules/home-mate-creation.js +73 -1
  112. package/lib/public/modules/personal-settings-dialog.js +7 -1
  113. package/lib/public/modules/project-access-recovery.js +17 -0
  114. package/lib/public/modules/project-admin-overview.js +21 -0
  115. package/lib/public/modules/session-folder-layout.js +1 -1
  116. package/lib/public/modules/session-folder-view-menu.js +9 -0
  117. package/lib/public/modules/session-github.js +65 -8
  118. package/lib/public/modules/sidebar-session-folders-ui.js +1 -1
  119. package/lib/public/modules/sticky-note-markdown.js +72 -6
  120. package/lib/public/modules/sticky-notes-actions.js +30 -0
  121. package/lib/public/modules/sticky-notes-browser.js +9 -2
  122. package/lib/public/modules/sticky-notes-card.js +23 -7
  123. package/lib/public/modules/sticky-notes-checklist.js +71 -0
  124. package/lib/public/modules/sticky-notes-editor.js +108 -44
  125. package/lib/public/modules/sticky-notes-shared.js +50 -2
  126. package/lib/public/modules/sticky-notes-state.js +66 -0
  127. package/lib/public/modules/sticky-notes.js +34 -51
  128. package/lib/public/modules/thinking-lifecycle.js +33 -3
  129. package/lib/public/modules/thinking-view.js +23 -1
  130. package/lib/public/modules/tools.js +34 -6
  131. package/lib/public/modules/user-settings-appearance.js +10 -3
  132. package/lib/public/modules/user-settings-editors.js +1 -1
  133. package/lib/public/modules/user-settings-layout.js +11 -2
  134. package/lib/public/modules/user-settings-preferences.js +6 -2
  135. package/lib/public/modules/user-settings-profile.js +12 -2
  136. package/lib/public/modules/user-settings.js +3 -2
  137. package/lib/public/style.css +2 -0
  138. package/lib/scheduled-task-records.js +3 -1
  139. package/lib/scheduler.js +2 -0
  140. package/lib/sdk-bridge.js +220 -21
  141. package/lib/sdk-message-processor.js +2 -0
  142. package/lib/sdk-response-recovery.js +155 -0
  143. package/lib/server-admin.js +63 -16
  144. package/lib/server-dm.js +1 -1
  145. package/lib/server-global-ws.js +10 -20
  146. package/lib/server-home-chat-events.js +7 -3
  147. package/lib/server-home-chat.js +4 -16
  148. package/lib/server-home-mate-creation.js +1 -0
  149. package/lib/server-home-mcp-consent.js +3 -3
  150. package/lib/server-mates.js +25 -67
  151. package/lib/server-owner-ui.js +77 -0
  152. package/lib/server-path-access.js +38 -0
  153. package/lib/server-project-removal.js +22 -0
  154. package/lib/server.js +99 -29
  155. package/lib/session-config-transition.js +45 -0
  156. package/lib/session-folders.js +7 -2
  157. package/lib/session-github.js +9 -2
  158. package/lib/session-visibility.js +5 -2
  159. package/lib/sessions.js +10 -0
  160. package/lib/terminal-manager.js +52 -0
  161. package/lib/users-permissions.js +2 -8
  162. package/lib/users.js +4 -0
  163. package/lib/workspace-query-access.js +4 -1
  164. package/lib/ws-schema.js +8 -2
  165. package/lib/yoke/adapters/claude.js +47 -8
  166. package/lib/yoke/adapters/codex.js +52 -111
  167. package/lib/yoke/claude-worker-startup.js +28 -0
  168. package/lib/yoke/codex-idle-recovery.js +81 -1
  169. package/lib/yoke/codex-recovery-admission.js +164 -0
  170. package/lib/yoke/session-tool-transport.js +3 -0
  171. package/package.json +1 -1
@@ -118,14 +118,20 @@ function attachSessions(ctx) {
118
118
  var ensureProjectAccessForSession = ctx.ensureProjectAccessForSession;
119
119
  var getOsUserInfoForWs = ctx.getOsUserInfoForWs;
120
120
  var hydrateSkillCatalog = ctx.hydrateSkillCatalog || function () { return []; };
121
+ var sessionActions = require("./project-session-actions").attachSessionActions(ctx);
122
+ var sessionVendor = require("./project-session-vendor").attachSessionVendor(ctx);
123
+ var sessionRuntime = require("./project-session-runtime-actions").attachSessionRuntimeActions({ sm: sm, tm: tm, prepareGui: prepareTuiSessionForGuiView, isPendingQueuePaused: ctx.isPendingQueuePaused,
124
+ publishGui: function (session) {
125
+ clients.forEach(function (ws) {
126
+ if (ws.readyState !== 1 || ws._clayActiveSession !== session.localId) return;
127
+ if (usersModule.isMultiUser() && (!ws._clayUser || !usersModule.findUserById(ws._clayUser.id) || !usersModule.canAccessProject(ws._clayUser.id, getProjectAccess()) || !usersModule.canAccessSession(ws._clayUser.id, session, getProjectAccess()))) return;
128
+ sm.switchSession(session.localId, ws, ctx.hydrateImageRefs);
129
+ });
130
+ },
131
+ });
121
132
  function stopSession(session, actor) {
122
133
  if (!session) return false;
123
- session._pendingMessageDrainPaused = true;
124
- if (typeof ctx.onPendingQueuePause === "function") ctx.onPendingQueuePause(session, actor || null);
125
- if (!session.isProcessing && !session._queryStarting) return true;
126
- if (typeof ctx.onHumanPairStop === "function") ctx.onHumanPairStop(session);
127
- session.taskStopRequested = true;
128
- if (session.abortController) session.abortController.abort();
134
+ sessionActions.stop(session, actor);
129
135
  return true;
130
136
  }
131
137
  var hydrateImageRefs = ctx.hydrateImageRefs;
@@ -149,9 +155,9 @@ function attachSessions(ctx) {
149
155
  var answers = msg.answers || {};
150
156
  var pending = session.pendingAskUser && session.pendingAskUser[toolId];
151
157
  if (!pending) return false;
152
- if (pending.request && pending.request.source === "mcp-import" &&
158
+ if (pending.request && ["mcp-import", "product-control"].indexOf(pending.request.source) >= 0 &&
153
159
  (typeof pending.request.authorizeResponse !== "function" || !pending.request.authorizeResponse(ws))) return false;
154
- if (pending.request && pending.request.source === "work-primary" && usersModule.isMultiUser() &&
160
+ if (pending.request && ["work-primary", "mate-schedule"].indexOf(pending.request.source) >= 0 && usersModule.isMultiUser() &&
155
161
  (!ws || !ws._clayUser || ws._clayUser.id !== session.ownerId)) return false;
156
162
  if (pending.mode === "yoke") {
157
163
  try { yoke.userInput.normalizeAnswers(pending.request.questions, answers); }
@@ -213,6 +219,10 @@ function attachSessions(ctx) {
213
219
  var setLatestVersion = ctx.setLatestVersion;
214
220
  var loadContextSources = ctx.loadContextSources;
215
221
 
222
+ var sessionHistory = require("./project-session-history-actions").attachSessionHistoryActions(Object.assign({}, ctx, {
223
+ resolveSessionHome: resolveSessionHome, cancelPendingAskUser: cancelPendingAskUser,
224
+ }));
225
+
216
226
  function canAccessProjectSlug(ws, targetSlug) {
217
227
  if (!targetSlug) return false;
218
228
  if (!usersModule || typeof usersModule.isMultiUser !== "function" || !usersModule.isMultiUser()) return true;
@@ -440,7 +450,7 @@ function attachSessions(ctx) {
440
450
  // Idempotent: if the PTY is already gone and history is populated, this
441
451
  // is a no-op (avoids re-reading jsonl and re-writing the session file on
442
452
  // every click while the user is reading the session in GUI mode).
443
- function prepareTuiSessionForGuiView(session) {
453
+ function prepareTuiSessionForGuiView(session, options) {
444
454
  if (!session || session.cliSessionId == null) return;
445
455
  var cliSess;
446
456
  try { cliSess = require("./cli-sessions"); } catch (e) { return; }
@@ -467,7 +477,7 @@ function attachSessions(ctx) {
467
477
  try { tm.close(session.terminalId); } catch (e) {}
468
478
  }
469
479
  session.terminalId = null;
470
- try { sm.saveSessionFile(session); } catch (e) {}
480
+ if (!options || !options.deferSave) { try { sm.saveSessionFile(session); } catch (e) {} }
471
481
  }
472
482
 
473
483
  // Resolve how a session should be presented to a viewer WITHOUT spawning a
@@ -768,11 +778,10 @@ function attachSessions(ctx) {
768
778
  sendTo(ws, { type: "error", text: "Cannot transfer ownership of home directory projects." });
769
779
  return true;
770
780
  }
771
- var projectOwnerId = getProjectOwnerId();
772
- var isAdmin = ws._clayUser && ws._clayUser.role === "admin";
781
+ var projectOwnerId = getProjectAccess().ownerId;
773
782
  var isProjectOwner = ws._clayUser && projectOwnerId && ws._clayUser.id === projectOwnerId;
774
- if (!ws._clayUser || (!isAdmin && !isProjectOwner)) {
775
- sendTo(ws, { type: "error", text: "Only project owners or admins can transfer ownership." });
783
+ if (!ws._clayUser || !isProjectOwner) {
784
+ sendTo(ws, { type: "error", text: "Only project owners can transfer ownership." });
776
785
  return true;
777
786
  }
778
787
  var targetUser = msg.userId ? usersModule.findUserById(msg.userId) : null;
@@ -780,11 +789,11 @@ function attachSessions(ctx) {
780
789
  sendTo(ws, { type: "error", text: "User not found." });
781
790
  return true;
782
791
  }
792
+ // Persist before publishing runtime ownership.
793
+ if (!opts.onProjectOwnerChanged) return true;
794
+ var savedOwner = opts.onProjectOwnerChanged(slug, targetUser.id);
795
+ if (savedOwner && savedOwner.error) { sendTo(ws, { type: "error", text: savedOwner.error }); return true; }
783
796
  setProjectOwnerId(targetUser.id);
784
- // Persist via daemon callback
785
- if (opts.onProjectOwnerChanged) {
786
- opts.onProjectOwnerChanged(slug, targetUser.id);
787
- }
788
797
  send({ type: "project_owner_changed", ownerId: targetUser.id, ownerName: targetUser.displayName || targetUser.username });
789
798
  return true;
790
799
  }
@@ -801,6 +810,7 @@ function attachSessions(ctx) {
801
810
  // broadcasts session_switched. A live runtime keeps its actual mode
802
811
  // for every viewer; idle sessions open in chat. Nothing is spawned here.
803
812
  var xmTarget = sm.sessions.get(msg.id);
813
+ if (usersModule.isMultiUser() && (!ws._clayUser || !usersModule.canAccessSession(ws._clayUser.id, xmTarget, getProjectAccess()))) return true;
804
814
  if (xmTarget && (!xmTarget.vendor || vendorSupportsTui(xmTarget.vendor))) {
805
815
  // Single source of truth: live runtime wins (tui stays tui, gui stays
806
816
  // gui); idle legacy sessions hydrate their transcript into chat.
@@ -939,19 +949,7 @@ function attachSessions(ctx) {
939
949
  var s = sm.sessions.get(msg.id);
940
950
  if (usersModule.isMultiUser() && (!ws._clayUser ||
941
951
  !usersModule.canAccessSession(ws._clayUser.id, s, getProjectAccess()))) return true;
942
- s.title = String(msg.title).substring(0, 100);
943
- s.titleProvisional = false;
944
- s.titleAutoGenerated = false;
945
- s.titleManuallySet = true;
946
- sm.saveSessionFile(s);
947
- sm.notifySessionRenamed(s.localId);
948
- sm.broadcastSessionList();
949
- // Sync title to SDK session
950
- if (s.cliSessionId) {
951
- adapter.renameSession(s.cliSessionId, s.title, { dir: cwd }).catch(function(e) {
952
- console.error("[project] SDK renameSession failed:", e.message);
953
- });
954
- }
952
+ sessionActions.rename(s, String(msg.title).substring(0, 100));
955
953
  }
956
954
  return true;
957
955
  }
@@ -1086,7 +1084,6 @@ function attachSessions(ctx) {
1086
1084
 
1087
1085
  if (msg.type === "stop") {
1088
1086
  var session = getSessionForWs(ws);
1089
- if (autonomousRun && session && session.autonomousRun) autonomousRun.finish(session, "stopped", "Stopped by the user.");
1090
1087
  stopSession(session, ws && ws._clayUser);
1091
1088
  return true;
1092
1089
  }
@@ -1187,38 +1184,14 @@ function attachSessions(ctx) {
1187
1184
 
1188
1185
  if (msg.type === "set_vendor" && msg.vendor) {
1189
1186
  var vendorSession = getSessionForWs(ws);
1190
- if (vendorSession) {
1191
- // Refuse to rebind vendor on a session that is already bound to a
1192
- // different CLI (cliSessionId is vendor-specific). This prevents a
1193
- // stale client-side vendor state from clobbering the persisted vendor
1194
- // on page reload / server restart.
1195
- var alreadyBound = vendorSession.cliSessionId && vendorSession.vendor && vendorSession.vendor !== msg.vendor;
1196
- if (alreadyBound) {
1197
- console.warn("[project] set_vendor ignored: session " + vendorSession.localId +
1198
- " is bound to '" + vendorSession.vendor + "', refused rebind to '" + msg.vendor + "'");
1199
- } else {
1200
- vendorSession.vendor = msg.vendor;
1201
- if (vendorSession.permissionMode === "auto" && msg.vendor !== "claude") {
1202
- vendorSession.permissionMode = "default";
1203
- vendorSession.effectivePermissionMode = null;
1204
- }
1205
- if (vendorSession.vendor !== msg.vendor) vendorSession.model = "";
1206
- sm.saveSessionFile(vendorSession);
1207
- sm.broadcastSessionList();
1208
- }
1209
- }
1210
- if (msg.vendor) {
1211
- var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[msg.vendor]) || [];
1212
- sendTo(ws, {
1213
- type: "model_info",
1214
- model: "",
1215
- models: vendorModels,
1216
- vendor: msg.vendor, sessionId: vendorSession ? vendorSession.localId : null,
1217
- availableVendors: sm.availableVendors || [],
1218
- installedVendors: sm.installedVendors || [],
1219
- });
1220
- send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1187
+ if (!vendorSession) return true;
1188
+ // Re-selecting the provider of an established thread does not rebind it.
1189
+ if (vendorSession.cliSessionId && vendorSession.vendor === msg.vendor) return true;
1190
+ try {
1191
+ var vendorResult = sessionVendor.setVendor(vendorSession, msg.vendor);
1192
+ if (vendorResult.status === "failed") sendTo(ws, { type: "error", text: vendorResult.error });
1221
1193
  }
1194
+ catch (error) { sendTo(ws, { type: "error", text: error.message }); }
1222
1195
  return true;
1223
1196
  }
1224
1197
 
@@ -1286,15 +1259,7 @@ function attachSessions(ctx) {
1286
1259
  if (!valid.ok) { permissionResult(false, valid.error, true); return true; }
1287
1260
  (async function() {
1288
1261
  try {
1289
- if (session.permissionMode === "bypassPermissions" && valid.mode !== "bypassPermissions" && autonomousRun) {
1290
- await autonomousRun.stopForPermissionChange(session);
1291
- }
1292
- await fullAccessService.setMode(session, valid.mode);
1293
- var state = permissionModes.clientState(session, sm.currentPermissionMode, sm.defaultVendor);
1294
- sm.sendToSession(session, { type: "config_state", sessionId: session.localId, vendor: session.vendor || sm.defaultVendor || "claude", mode: state.requestedPermissionMode,
1295
- effectivePermissionMode: state.effectivePermissionMode, permissionRequestId: requestId,
1296
- permissionCapabilities: state.permissionCapabilities, mcpPermissionModeOverrides: state.mcpPermissionModeOverrides,
1297
- effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1262
+ await sessionActions.permission(session, valid.mode, requestId);
1298
1263
  permissionResult(true, null, true);
1299
1264
  } catch (error) {
1300
1265
  permissionResult(false, "Failed to change permission mode: " + (error.message || error), true);
@@ -1364,10 +1329,9 @@ function attachSessions(ctx) {
1364
1329
  sendTo(ws, { type: "error", text: "This effort level is not supported by the selected vendor." });
1365
1330
  return true;
1366
1331
  }
1367
- sdk.setEffort(session, sessionEffort).catch(function (e) {
1332
+ sessionActions.effort(session, sessionEffort).catch(function (e) {
1368
1333
  sendTo(ws, { type: "error", text: "Failed to change reasoning effort: " + (e.message || e) });
1369
1334
  });
1370
- sm.sendToSession(session, { type: "config_state", model: session.model || ((sm.defaultModelByVendor || {})[effortVendor]) || "", vendor: effortVendor, sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: sessionEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1371
1335
  }
1372
1336
  return true;
1373
1337
  }
@@ -1444,92 +1408,9 @@ function attachSessions(ctx) {
1444
1408
  sendTo(ws, { type: "rewind_error", text: "Rewind already in progress." });
1445
1409
  return true;
1446
1410
  }
1447
- session._rewindInProgress = true;
1448
- var mode = msg.mode || "both";
1449
-
1450
- (async function () {
1451
- try {
1452
- // File restoration (delegated to adapter via sdk-bridge)
1453
- if (mode !== "chat") {
1454
- await sdk.rewindExecuteFiles(session, msg.uuid);
1455
- }
1456
-
1457
- // Conversation rollback (skip for files-only mode)
1458
- if (mode !== "files") {
1459
- var targetIdx = -1;
1460
- for (var i = 0; i < session.messageUUIDs.length; i++) {
1461
- if (session.messageUUIDs[i].uuid === msg.uuid) {
1462
- targetIdx = i;
1463
- break;
1464
- }
1465
- }
1466
-
1467
- // Codex's thread/revert cuts at a turn id, not a count. Read the
1468
- // target entry's turnId BEFORE trimming local history below.
1469
- var hasTurnsToRollBack = false;
1470
- var targetTurnId = null;
1471
- if (targetIdx >= 0) {
1472
- targetTurnId = session.messageUUIDs[targetIdx].turnId || null;
1473
- for (var ri = targetIdx; ri < session.messageUUIDs.length; ri++) {
1474
- if (session.messageUUIDs[ri].type === "user") { hasTurnsToRollBack = true; break; }
1475
- }
1476
- }
1477
-
1478
- // Notify adapter of conversation rollback (e.g. Codex thread/revert)
1479
- // BEFORE trimming local history below. Deliberately not caught
1480
- // here: a failed or impossible (missing turnId) provider-side
1481
- // rollback must abort the whole rewind via the outer catch, never
1482
- // leave local history trimmed ahead of the real provider thread.
1483
- if (hasTurnsToRollBack) {
1484
- await sdk.rollbackConversation(session, targetTurnId);
1485
- }
1486
-
1487
- if (targetIdx >= 0) {
1488
- var trimTo = session.messageUUIDs[targetIdx].historyIndex;
1489
- for (var k = trimTo - 1; k >= 0; k--) {
1490
- if (session.history[k].type === "user_message") {
1491
- trimTo = k;
1492
- break;
1493
- }
1494
- }
1495
- session.history = session.history.slice(0, trimTo);
1496
- session.messageUUIDs = session.messageUUIDs.slice(0, targetIdx);
1497
- // Reset digest checkpoint if it points past the trimmed history
1498
- if (typeof session._dmLastDigestedIndex === "number" && session._dmLastDigestedIndex > trimTo) {
1499
- session._dmLastDigestedIndex = trimTo;
1500
- }
1501
- }
1502
-
1503
- var kept = session.messageUUIDs;
1504
- session.lastRewindUuid = kept.length > 0 ? kept[kept.length - 1].uuid : null;
1505
- }
1506
-
1507
- if (session.abortController) {
1508
- try { session.abortController.abort(); } catch (e) {}
1509
- }
1510
- if (session.messageQueue) {
1511
- try { session.messageQueue.end(); } catch (e) {}
1512
- }
1513
- session.queryInstance = null;
1514
- session.messageQueue = null;
1515
- session.abortController = null;
1516
- session.blocks = {};
1517
- session.sentToolResults = {};
1518
- session.pendingPermissions = {};
1519
- cancelPendingAskUser(session, "Conversation rewound");
1520
- session.isProcessing = false;
1521
- onProcessingChanged();
1522
-
1523
- sm.saveSessionFile(session);
1524
- sm.switchSession(session.localId, ws, hydrateImageRefs);
1525
- sm.sendAndRecord(session, { type: "rewind_complete", mode: mode });
1526
- sm.broadcastSessionList();
1527
- } catch (err) {
1528
- sendTo(ws, { type: "rewind_error", text: "Rewind failed: " + err.message });
1529
- } finally {
1530
- session._rewindInProgress = false;
1531
- }
1532
- })();
1411
+ sessionHistory.rewind(session, msg.uuid, msg.mode || "both", ws, function () {}).catch(function (error) {
1412
+ sendTo(ws, { type: "rewind_error", text: "Rewind failed: " + error.message });
1413
+ });
1533
1414
  return true;
1534
1415
  }
1535
1416
 
@@ -1539,62 +1420,10 @@ function attachSessions(ctx) {
1539
1420
  sendTo(ws, { type: "error", text: "Cannot fork: no CLI session" });
1540
1421
  return true;
1541
1422
  }
1542
- var forkTitle = (session.title || "New Session") + " (fork)";
1543
-
1544
- sdk.forkSession(session, msg.uuid).then(function(result) {
1545
- if (result.useLocalHistory) {
1546
- // Copy local history up to the target UUID
1547
- var targetIdx = -1;
1548
- for (var fi = 0; fi < session.messageUUIDs.length; fi++) {
1549
- if (session.messageUUIDs[fi].uuid === msg.uuid) { targetIdx = fi; break; }
1550
- }
1551
- var forkHistory = [];
1552
- if (targetIdx >= 0) {
1553
- var trimTo = session.messageUUIDs[targetIdx].historyIndex;
1554
- forkHistory = session.history.slice(0, trimTo);
1555
- } else {
1556
- forkHistory = session.history.slice();
1557
- }
1558
- var forked = sm.createSession({
1559
- vendor: session.vendor,
1560
- ownerId: session.ownerId || null,
1561
- userKnowledgeOwnerId: session.userKnowledgeOwnerId || null,
1562
- sessionVisibility: sessionVisibility.normalize(session.sessionVisibility),
1563
- }, ws);
1564
- forked.cliSessionId = result.sessionId;
1565
- forked.title = forkTitle;
1566
- forked.history = forkHistory;
1567
- forked.messageUUIDs = [];
1568
- for (var hi = 0; hi < forkHistory.length; hi++) {
1569
- if (forkHistory[hi].type === "message_uuid") {
1570
- forked.messageUUIDs.push({ uuid: forkHistory[hi].uuid, type: forkHistory[hi].messageType, historyIndex: hi, turnId: forkHistory[hi].turnId || null });
1571
- }
1572
- }
1573
- sm.saveSessionFile(forked);
1574
- sm.switchSession(forked.localId, ws, hydrateImageRefs);
1575
- sendTo(ws, { type: "fork_complete", sessionId: forked.localId });
1576
- sm.broadcastSessionList();
1577
- } else {
1578
- // Read history from CLI session files
1579
- var cliSess = require("./cli-sessions");
1580
- return cliSess.readCliSessionHistory(resolveSessionHome(session), cwd, result.sessionId).then(function(history) {
1581
- var forked = sm.resumeSession(result.sessionId, {
1582
- history: history,
1583
- title: forkTitle,
1584
- vendor: session.vendor,
1585
- ownerId: session.ownerId || null,
1586
- userKnowledgeOwnerId: session.userKnowledgeOwnerId || null,
1587
- sessionVisibility: sessionVisibility.normalize(session.sessionVisibility),
1588
- sessionVisibilityExplicit: session.sessionVisibilityExplicit === true,
1589
- }, ws);
1590
- if (forked) {
1591
- ws._clayActiveSession = forked.localId;
1592
- sendTo(ws, { type: "fork_complete", sessionId: forked.localId });
1593
- }
1594
- });
1595
- }
1596
- }).catch(function(e) {
1597
- sendTo(ws, { type: "error", text: "Fork failed: " + (e.message || e) });
1423
+ sessionHistory.fork(session, msg.uuid, ws, function () {}).then(function (forked) {
1424
+ if (forked) sendTo(ws, { type: "fork_complete", sessionId: forked.localId });
1425
+ }).catch(function (error) {
1426
+ sendTo(ws, { type: "error", text: "Fork failed: " + error.message });
1598
1427
  });
1599
1428
  return true;
1600
1429
  }
@@ -1809,14 +1638,6 @@ function attachSessions(ctx) {
1809
1638
  if (msg.type === "browse_dir") {
1810
1639
  var rawPath = (msg.path || "").replace(/^~/, require("./config").REAL_HOME);
1811
1640
  var absTarget = path.resolve(rawPath);
1812
- // Multi-user mode: non-admins can only browse their home directory
1813
- if (osUsers && osUsers.length > 0 && ws._clayUser && ws._clayUser.role !== "admin") {
1814
- var browseHome = ws._clayUser.linuxUser ? "/home/" + ws._clayUser.linuxUser : null;
1815
- if (!browseHome || (absTarget !== browseHome && (absTarget + "/").indexOf(browseHome + "/") !== 0)) {
1816
- sendTo(ws, { type: "browse_dir_result", path: msg.path, entries: [], error: "Access restricted to your home directory" });
1817
- return true;
1818
- }
1819
- }
1820
1641
  var parentDir, prefix;
1821
1642
  try {
1822
1643
  var stat = fs.statSync(absTarget);
@@ -1833,12 +1654,17 @@ function attachSessions(ctx) {
1833
1654
  parentDir = path.dirname(absTarget);
1834
1655
  prefix = path.basename(absTarget).toLowerCase();
1835
1656
  }
1657
+ if (opts.canUsePath && !opts.canUsePath(ws._clayUser && ws._clayUser.id, parentDir)) {
1658
+ sendTo(ws, { type: "browse_dir_result", entries: [], error: "Directory access is not permitted" });
1659
+ return true;
1660
+ }
1836
1661
  try {
1837
1662
  var dirItems = fs.readdirSync(parentDir, { withFileTypes: true });
1838
1663
  var dirEntries = [];
1839
1664
  for (var di = 0; di < dirItems.length; di++) {
1840
1665
  var d = dirItems[di];
1841
1666
  if (!d.isDirectory()) continue;
1667
+ if (opts.canUsePath && !opts.canUsePath(ws._clayUser && ws._clayUser.id, path.join(parentDir, d.name))) continue;
1842
1668
  if (d.name.charAt(0) === ".") continue;
1843
1669
  if (IGNORED_DIRS.has(d.name)) continue;
1844
1670
  if (prefix && !d.name.toLowerCase().startsWith(prefix)) continue;
@@ -1856,17 +1682,9 @@ function attachSessions(ctx) {
1856
1682
  if (msg.type === "add_project") {
1857
1683
  var addPath = (msg.path || "").replace(/^~/, require("./config").REAL_HOME);
1858
1684
  var addAbs = path.resolve(addPath);
1859
- // Multi-user mode: normal users restricted to their home directory
1860
- if (osUsers && osUsers.length > 0 && ws._clayUser && ws._clayUser.role !== "admin") {
1861
- if (!ws._clayUser.linuxUser) {
1862
- sendTo(ws, { type: "add_project_result", ok: false, error: "No Linux user assigned" });
1863
- return true;
1864
- }
1865
- var userHome = "/home/" + ws._clayUser.linuxUser;
1866
- if (addAbs !== userHome && (addAbs + "/").indexOf(userHome + "/") !== 0) {
1867
- sendTo(ws, { type: "add_project_result", ok: false, error: "Path not allowed. You can only add directories under " + userHome });
1868
- return true;
1869
- }
1685
+ if (opts.canUsePath && !opts.canUsePath(ws._clayUser && ws._clayUser.id, addAbs)) {
1686
+ sendTo(ws, { type: "add_project_result", ok: false, error: "Directory access is not permitted" });
1687
+ return true;
1870
1688
  }
1871
1689
  try {
1872
1690
  var addStat = fs.statSync(addAbs);
@@ -1880,7 +1698,7 @@ function attachSessions(ctx) {
1880
1698
  }
1881
1699
  if (typeof opts.onAddProject === "function") {
1882
1700
  var result = opts.onAddProject(addAbs, ws._clayUser);
1883
- sendTo(ws, { type: "add_project_result", ok: result.ok, slug: result.slug, error: result.error, existing: result.existing });
1701
+ sendTo(ws, Object.assign({}, result, { type: "add_project_result" }));
1884
1702
  } else {
1885
1703
  sendTo(ws, { type: "add_project_result", ok: false, error: "Not supported" });
1886
1704
  }
@@ -1905,7 +1723,7 @@ function attachSessions(ctx) {
1905
1723
  }
1906
1724
  if (typeof opts.onCreateProject === "function") {
1907
1725
  var createResult = opts.onCreateProject(createName, ws._clayUser);
1908
- sendTo(ws, { type: "add_project_result", ok: createResult.ok, slug: createResult.slug, error: createResult.error });
1726
+ sendTo(ws, Object.assign({}, createResult, { type: "add_project_result" }));
1909
1727
  } else {
1910
1728
  sendTo(ws, { type: "add_project_result", ok: false, error: "Not supported" });
1911
1729
  }
@@ -1995,10 +1813,10 @@ function attachSessions(ctx) {
1995
1813
  moveAllSchedulesToProject(removeSlug, msg.moveTasksTo);
1996
1814
  }
1997
1815
  if (typeof opts.onRemoveProject === "function") {
1998
- // Send result before removing so the WS is still open
1999
- sendTo(ws, { type: "remove_project_result", ok: true, slug: removeSlug });
2000
1816
  var removeUserId = ws._clayUser ? ws._clayUser.id : null;
2001
- opts.onRemoveProject(removeSlug, removeUserId);
1817
+ Promise.resolve().then(function () { return opts.onRemoveProject(removeSlug, removeUserId); }).then(function (result) {
1818
+ sendTo(ws, { type: "remove_project_result", ok: !!result && result.ok === true, slug: removeSlug, error: result && result.error });
1819
+ }, function (error) { sendTo(ws, { type: "remove_project_result", ok: false, slug: removeSlug, error: error.message }); });
2002
1820
  } else {
2003
1821
  sendTo(ws, { type: "remove_project_result", ok: false, error: "Not supported" });
2004
1822
  }
@@ -2044,7 +1862,7 @@ function attachSessions(ctx) {
2044
1862
  }
2045
1863
  if (typeof opts.onSetProjectTitle === "function") {
2046
1864
  var titleResult = opts.onSetProjectTitle(msg.slug, msg.title || null);
2047
- sendTo(ws, { type: "set_project_title_result", ok: titleResult.ok, slug: msg.slug, error: titleResult.error });
1865
+ sendTo(ws, Object.assign({}, titleResult, { type: "set_project_title_result", slug: msg.slug }));
2048
1866
  } else {
2049
1867
  sendTo(ws, { type: "set_project_title_result", ok: false, error: "Not supported" });
2050
1868
  }
@@ -2198,6 +2016,11 @@ function attachSessions(ctx) {
2198
2016
  return handleSessionsMessage(ws, { type: "permission_response", requestId: msg.requestId, decision: msg.decision });
2199
2017
  },
2200
2018
  stopHomeSession: stopSession,
2019
+ sessionActions: sessionActions,
2020
+ sessionHistoryActions: sessionHistory,
2021
+ sessionVendorActions: sessionVendor,
2022
+ sessionRuntimeActions: sessionRuntime,
2023
+ deleteProductControlPlan: function (plan) { sessionDelete.executeOwnedDeletion(plan); },
2201
2024
  };
2202
2025
  }
2203
2026
 
@@ -118,6 +118,12 @@ function attachUserMessage(ctx) {
118
118
  requestId: msg.requestId || null, ok: !!ok, error: error || null });
119
119
  }
120
120
 
121
+ function noteUpdateResult(ws, msg, ok, note, error) {
122
+ if (!msg.requestId) return;
123
+ sendTo(ws, { type: "note_update_result", projectSlug: slug, noteId: msg.id || null,
124
+ requestId: msg.requestId, ok: !!ok, note: note || null, error: error || null });
125
+ }
126
+
121
127
  // --------------- Main handler ---------------
122
128
 
123
129
  function handleUserMessage(ws, msg, forcedSession, internal) {
@@ -155,7 +161,10 @@ function attachUserMessage(ctx) {
155
161
  }
156
162
 
157
163
  if (msg.type === "note_update") {
158
- if (!msg.id) return true;
164
+ if (!msg.id) {
165
+ noteUpdateResult(ws, msg, false, null, "Note id is required");
166
+ return true;
167
+ }
159
168
  // A legacy `hidden` update is a lifecycle transition, so the actor is
160
169
  // derived from the bound socket rather than taken from the message.
161
170
  var noteChanges = msg;
@@ -166,10 +175,24 @@ function attachUserMessage(ctx) {
166
175
  if (updated) {
167
176
  send({ type: "note_updated", note: updated });
168
177
  if (msg.text !== undefined || msg.hidden !== undefined) syncNotesKnowledge();
178
+ noteUpdateResult(ws, msg, true, updated, null);
179
+ } else {
180
+ noteUpdateResult(ws, msg, false, null, "Note update could not be saved");
169
181
  }
170
182
  return true;
171
183
  }
172
184
 
185
+ if (msg.type === "note_close" && msg.humanClose === true) {
186
+ if (!canHumanManageNotes(ws)) { noteUpdateResult(ws, msg, false, null, "You are not authorized to close notes in this project."); return true; }
187
+ var result = nm.closeHuman(msg.id, msg, notesLifecycle.userActor(ws && ws._clayUser));
188
+ if (!result) { noteUpdateResult(ws, msg, false, null, "The note changed or could not be saved. Review it and retry."); return true; }
189
+ if (result.removed) send({ type: "note_deleted", id: result.removed.id });
190
+ else send({ type: "note_updated", note: result.note });
191
+ noteUpdateResult(ws, msg, true, result.note || null, null);
192
+ syncNotesKnowledge();
193
+ return true;
194
+ }
195
+
173
196
  // Completing a note closes it. `note_delete` is the retired spelling and is
174
197
  // deliberately routed to the same non-destructive close, so an older client
175
198
  // still on the wire cannot erase a record.
@@ -443,6 +466,10 @@ function attachUserMessage(ctx) {
443
466
  finishPendingEarly(false, "Pending messages cannot be delivered to a Driver-operated Split Worker");
444
467
  return true;
445
468
  }
469
+ if (!pendingInternal && ctx.resumeLifecycleProject) {
470
+ try { ctx.resumeLifecycleProject(ws); }
471
+ catch (error) { sendTo(ws, { type: "error", text: error.message }); return true; }
472
+ }
446
473
  if (!pendingInternal && session._driverContinuationLease && pendingMessageQueue) {
447
474
  var continuationQueued = pendingMessageQueue.admit(session, msg, ws && ws._clayUser);
448
475
  if (continuationQueued.ok) {
@@ -719,6 +746,7 @@ function attachUserMessage(ctx) {
719
746
 
720
747
  function pendingBlockReason() {
721
748
  if (!pendingInternal) return null;
749
+ if (ctx.isLifecycleExecutionPaused && ctx.isLifecycleExecutionPaused()) return "Pending message dispatch paused";
722
750
  if (!pendingMessageQueue.isClaimed(session, pendingInternal.item.id)) return "Pending message was cancelled";
723
751
  if (pendingMessageQueue.isPaused(session) || session._pendingMessageDrainPaused || session.taskStopRequested) return "Pending message dispatch paused";
724
752
  if (typeof ctx.authorizePendingDispatch !== "function" || !ctx.authorizePendingDispatch(session, ws && ws._clayUser, pendingInternal.item)) return "Pending message authorization changed";
@@ -941,6 +969,7 @@ function attachUserMessage(ctx) {
941
969
  }
942
970
 
943
971
  function consumePendingMessage(session) {
972
+ if (ctx.isLifecycleExecutionPaused && ctx.isLifecycleExecutionPaused()) return false;
944
973
  if (!pendingMessageQueue) return false;
945
974
  var consumed = pendingMessageQueue.consumeOne(session, function (item, complete) {
946
975
  var actor = item.actorId && usersModule.findUserById(item.actorId);