clay-server 4.1.0-beta.8 → 4.1.0

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 (119) hide show
  1. package/lib/context-usage.js +47 -0
  2. package/lib/issues-mcp-server.js +1 -1
  3. package/lib/issues-service.js +3 -1
  4. package/lib/loop-registry-occurrence.js +1 -0
  5. package/lib/notes.js +20 -3
  6. package/lib/os-users.js +6 -0
  7. package/lib/project-connection.js +14 -3
  8. package/lib/project-file-path.js +13 -0
  9. package/lib/project-file-watch.js +19 -8
  10. package/lib/project-filesystem.js +10 -16
  11. package/lib/project-http.js +53 -48
  12. package/lib/project-logs-mcp-server.js +8 -8
  13. package/lib/project-logs-schema.js +1 -1
  14. package/lib/project-logs-service.js +46 -13
  15. package/lib/project-logs-store.js +17 -15
  16. package/lib/project-logs.js +70 -0
  17. package/lib/project-loop.js +1 -0
  18. package/lib/project-notifications.js +1 -1
  19. package/lib/project-pending-message-queue.js +71 -7
  20. package/lib/project-scheduled-messages.js +235 -52
  21. package/lib/project-scheduled-result-store.js +131 -0
  22. package/lib/project-scheduled-task-execution.js +35 -2
  23. package/lib/project-session-notes.js +11 -4
  24. package/lib/project-sessions.js +7 -0
  25. package/lib/project-user-message.js +72 -5
  26. package/lib/project.js +60 -5
  27. package/lib/public/app.js +27 -12
  28. package/lib/public/css/avatar-imprints.css +1 -1
  29. package/lib/public/css/command-palette.css +20 -0
  30. package/lib/public/css/git-panel.css +13 -12
  31. package/lib/public/css/git-placard.css +4 -4
  32. package/lib/public/css/input.css +0 -109
  33. package/lib/public/css/issues.css +78 -31
  34. package/lib/public/css/mobile-nav.css +7 -0
  35. package/lib/public/css/notifications-center.css +3 -3
  36. package/lib/public/css/pending-message-queue.css +3 -0
  37. package/lib/public/css/project-logs.css +15 -0
  38. package/lib/public/css/sidebar.css +7 -0
  39. package/lib/public/css/sticky-notes.css +29 -0
  40. package/lib/public/modules/app-connection.js +9 -1
  41. package/lib/public/modules/app-favicon.js +10 -0
  42. package/lib/public/modules/app-messages.js +49 -37
  43. package/lib/public/modules/app-notifications.js +2 -0
  44. package/lib/public/modules/app-panels.js +77 -23
  45. package/lib/public/modules/app-projects.js +3 -0
  46. package/lib/public/modules/app-rate-limit.js +148 -243
  47. package/lib/public/modules/avatar-imprint.js +25 -70
  48. package/lib/public/modules/avatar.js +12 -4
  49. package/lib/public/modules/chat-render-runtime.js +20 -2
  50. package/lib/public/modules/clay-issue-links.js +23 -2
  51. package/lib/public/modules/clay-log-links.js +11 -1
  52. package/lib/public/modules/context-view-preference.js +143 -0
  53. package/lib/public/modules/default-vendor.js +116 -0
  54. package/lib/public/modules/filebrowser-tabs.js +1 -0
  55. package/lib/public/modules/filebrowser.js +45 -18
  56. package/lib/public/modules/input.js +81 -20
  57. package/lib/public/modules/issues-render.js +36 -10
  58. package/lib/public/modules/issues.js +95 -13
  59. package/lib/public/modules/markdown.js +3 -1
  60. package/lib/public/modules/pane-bridge.js +14 -8
  61. package/lib/public/modules/pane-issue-bridge.js +27 -0
  62. package/lib/public/modules/pending-message-queue.js +43 -1
  63. package/lib/public/modules/permission-hints.js +13 -0
  64. package/lib/public/modules/profile.js +9 -9
  65. package/lib/public/modules/project-activation.js +49 -0
  66. package/lib/public/modules/project-logs-render.js +13 -3
  67. package/lib/public/modules/project-logs.js +45 -15
  68. package/lib/public/modules/right-workbench.js +44 -0
  69. package/lib/public/modules/scheduled-message-state.js +12 -36
  70. package/lib/public/modules/scheduled-result-client.js +33 -0
  71. package/lib/public/modules/scheduled-result-display.js +50 -0
  72. package/lib/public/modules/scheduled-result-notification.js +31 -0
  73. package/lib/public/modules/scheduled-task-presentation.js +15 -0
  74. package/lib/public/modules/scheduled-tasks.js +9 -21
  75. package/lib/public/modules/sidebar-mobile.js +51 -3
  76. package/lib/public/modules/sidebar-sessions.js +65 -5
  77. package/lib/public/modules/split-session-boundary.js +28 -0
  78. package/lib/public/modules/split-view.js +42 -7
  79. package/lib/public/modules/sticky-notes-browser.js +130 -18
  80. package/lib/public/modules/sticky-notes.js +2 -3
  81. package/lib/public/modules/terminal.js +4 -4
  82. package/lib/public/modules/thinking-lifecycle.js +4 -1
  83. package/lib/public/modules/tools.js +5 -1
  84. package/lib/scheduled-run-state.js +10 -2
  85. package/lib/scheduler.js +1 -0
  86. package/lib/sdk-bridge.js +97 -61
  87. package/lib/sdk-message-processor.js +44 -13
  88. package/lib/sdk-skill-discovery.js +6 -28
  89. package/lib/sdk-turn-queue.js +14 -0
  90. package/lib/server-context-view.js +53 -0
  91. package/lib/server-default-vendor.js +60 -0
  92. package/lib/server.js +23 -6
  93. package/lib/session-notes-mcp-server.js +3 -5
  94. package/lib/session-pair-factory.js +2 -2
  95. package/lib/session-pair-mcp-server.js +5 -4
  96. package/lib/session-title-generator.js +89 -0
  97. package/lib/session-title-policy.js +8 -2
  98. package/lib/sessions.js +26 -1
  99. package/lib/user-presence.js +6 -0
  100. package/lib/users-context-view-preferences.js +65 -0
  101. package/lib/users-default-vendor-preferences.js +80 -0
  102. package/lib/users.js +12 -0
  103. package/lib/ws-schema.js +11 -0
  104. package/lib/yoke/acp-agent-profiles.js +15 -2
  105. package/lib/yoke/adapters/acp.js +3 -2
  106. package/lib/yoke/adapters/antigravity.js +2 -2
  107. package/lib/yoke/adapters/claude-worker.js +11 -12
  108. package/lib/yoke/adapters/claude.js +75 -22
  109. package/lib/yoke/adapters/codex.js +216 -29
  110. package/lib/yoke/adapters/kiro.js +6 -5
  111. package/lib/yoke/claude-context-usage.js +57 -0
  112. package/lib/yoke/claude-message-correlation.js +31 -0
  113. package/lib/yoke/claude-model-switch.js +15 -0
  114. package/lib/yoke/index.js +5 -1
  115. package/lib/yoke/mcp-bridge-server.js +18 -5
  116. package/lib/yoke/session-skill-catalog.js +45 -0
  117. package/lib/yoke/skill-discovery.js +60 -20
  118. package/lib/yoke/vendor-registry.js +1 -1
  119. package/package.json +5 -7
@@ -0,0 +1,53 @@
1
+ // Authenticated user-wide context display preference service.
2
+
3
+ function attachContextViewService(deps) {
4
+ var users = deps.users;
5
+ var forEachAppClient = deps.forEachAppClient;
6
+ var revisions = {};
7
+ var epoch = deps.serverEpoch || require("crypto").randomBytes(12).toString("hex");
8
+
9
+ function actor(ws) {
10
+ if (users.isMultiUser() !== true) return { userId: "default", user: true };
11
+ var claimed = ws && ws._clayUser && ws._clayUser.id;
12
+ var user = claimed ? users.findUserById(claimed) : null;
13
+ return user ? { userId: user.id, user: user } : { userId: null, user: null };
14
+ }
15
+ function sameActor(ws, userId) {
16
+ var current = actor(ws);
17
+ return current.user && current.userId === userId;
18
+ }
19
+ function send(ws, message) {
20
+ if (ws && ws.readyState === 1) ws.send(JSON.stringify(Object.assign({ serverEpoch: epoch }, message)));
21
+ }
22
+ function revision(userId) { return revisions[userId] || 0; }
23
+ function broadcast(userId, message, except) {
24
+ forEachAppClient(function (client) { if (client === except || !sameActor(client, userId)) return; send(client, message); });
25
+ }
26
+ function handleMessage(ws, msg) {
27
+ if (!msg || ["context_view_get", "context_view_set"].indexOf(msg.type) === -1) return false;
28
+ var requestId = typeof msg.requestId === "string" ? msg.requestId.slice(0, 200) : null;
29
+ var who = actor(ws);
30
+ if (!who.user) {
31
+ send(ws, { type: "context_view_state", requestId: requestId, accountAvailable: false, accountId: null, mode: null, error: "Your account is no longer available." });
32
+ return true;
33
+ }
34
+ if (msg.type === "context_view_get") {
35
+ var current = users.getContextViewPreference(who.userId);
36
+ send(ws, { type: "context_view_state", requestId: requestId, accountAvailable: true, accountId: who.userId, mode: current.mode, preferencePresent: current.present, canonicalRevision: revision(who.userId), ready: !current.error, error: current.error || "" });
37
+ return true;
38
+ }
39
+ var saved = users.setContextViewPreference(who.userId, msg.mode);
40
+ if (!saved.ok) {
41
+ send(ws, { type: "context_view_state", requestId: requestId, accountAvailable: true, accountId: who.userId, canonicalRevision: revision(who.userId), ready: false, error: saved.error });
42
+ return true;
43
+ }
44
+ revisions[who.userId] = revision(who.userId) + 1;
45
+ var state = { type: "context_view_state", requestId: null, accountAvailable: true, accountId: who.userId, mode: msg.mode, preferencePresent: true, canonicalRevision: revisions[who.userId], ready: true, error: "" };
46
+ broadcast(who.userId, state, ws);
47
+ send(ws, Object.assign({}, state, { requestId: requestId }));
48
+ return true;
49
+ }
50
+ return { handleMessage: handleMessage };
51
+ }
52
+
53
+ module.exports = { attachContextViewService: attachContextViewService };
@@ -0,0 +1,60 @@
1
+ // Authenticated user-wide New session vendor preference service.
2
+
3
+ function attachDefaultVendorService(deps) {
4
+ var users = deps.users;
5
+ var homeChatHandler = deps.homeChatHandler;
6
+ var forEachAppClient = deps.forEachAppClient;
7
+ var revisions = {};
8
+ var epoch = deps.serverEpoch || require("crypto").randomBytes(12).toString("hex");
9
+
10
+ function multiUser() { return users.isMultiUser() === true; }
11
+ function actor(ws) {
12
+ if (!multiUser()) return { userId: "default", user: true };
13
+ var claimed = ws && ws._clayUser && ws._clayUser.id;
14
+ var user = claimed ? users.findUserById(claimed) : null;
15
+ return user ? { userId: user.id, user: user } : { userId: null, user: null };
16
+ }
17
+ function sameActor(ws, userId) { var current = actor(ws); return current.user && current.userId === userId; }
18
+ function send(ws, message) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(Object.assign({ serverEpoch: epoch }, message))); }
19
+ function revision(userId) { return revisions[userId] || 0; }
20
+ function installedVendors(ws, who) {
21
+ var found;
22
+ try { found = homeChatHandler.findMateProject(multiUser() ? who.userId : null, null, true); } catch (e) { return []; }
23
+ if (!found || !found.ctx || typeof found.ctx.getVendorModelAvailability !== "function") return [];
24
+ var socket = Object.create(ws || null);
25
+ socket._clayUser = who.user === true ? null : who.user;
26
+ var choices = found.ctx.getVendorModelAvailability(socket) || [];
27
+ return choices.filter(function (choice) { return choice && choice.installed === true; }).map(function (choice) { return choice.id; });
28
+ }
29
+ function broadcast(userId, message, except) {
30
+ forEachAppClient(function (client) { if (client === except || !sameActor(client, userId)) return; send(client, message); });
31
+ }
32
+ function handle(ws, msg, projectSlug) {
33
+ if (!msg || ["default_vendor_get", "default_vendor_set"].indexOf(msg.type) === -1) return false;
34
+ var requestId = typeof msg.requestId === "string" ? msg.requestId.slice(0, 200) : null;
35
+ var who = actor(ws);
36
+ if (!who.user) { send(ws, { type: "default_vendor_state", requestId: requestId, accountAvailable: false, accountId: null, projectSlug: projectSlug || null, installedVendors: [], preference: null, error: "Your account is no longer available." }); return true; }
37
+ var installed = installedVendors(ws, who);
38
+ if (msg.type === "default_vendor_get") {
39
+ var saved = users.getDefaultVendorPreference(who.userId);
40
+ send(ws, { type: "default_vendor_state", requestId: requestId, projectSlug: projectSlug || null, accountAvailable: true, accountId: who.userId, installedVendors: installed, preference: saved.vendor, preferencePresent: saved.present, ready: !saved.error, canonicalRevision: revision(who.userId), error: saved.error || "" });
41
+ return true;
42
+ }
43
+ if (msg.vendor !== "" && (typeof msg.vendor !== "string" || installed.indexOf(msg.vendor) === -1)) {
44
+ send(ws, { type: "default_vendor_state", requestId: requestId, projectSlug: projectSlug || null, accountAvailable: true, accountId: who.userId, installedVendors: installed, preference: users.getDefaultVendorPreference(who.userId).vendor, ready: false, canonicalRevision: revision(who.userId), error: "That vendor is not installed or authorized." });
45
+ return true;
46
+ }
47
+ var savedResult = users.setDefaultVendorPreference(who.userId, msg.vendor);
48
+ if (!savedResult.ok) { send(ws, { type: "default_vendor_state", requestId: requestId, projectSlug: projectSlug || null, accountAvailable: true, accountId: who.userId, installedVendors: installed, ready: false, error: savedResult.error }); return true; }
49
+ revisions[who.userId] = revision(who.userId) + 1;
50
+ var state = { type: "default_vendor_state", requestId: null, projectSlug: projectSlug || null, accountAvailable: true, accountId: who.userId, installedVendors: installed, preference: msg.vendor || null, preferencePresent: !!msg.vendor, ready: true, canonicalRevision: revisions[who.userId], error: "" };
51
+ var broadcastState = Object.assign({}, state);
52
+ delete broadcastState.projectSlug;
53
+ broadcast(who.userId, broadcastState, ws);
54
+ send(ws, Object.assign({}, state, { requestId: requestId }));
55
+ return true;
56
+ }
57
+ return { handleMessage: handle };
58
+ }
59
+
60
+ module.exports = { attachDefaultVendorService: attachDefaultVendorService };
package/lib/server.js CHANGED
@@ -17,6 +17,8 @@ var serverHomeChat = require("./server-home-chat");
17
17
  var shouldSuppressHomeResponse = require("./home-response-notifications").shouldSuppressHomeResponse;
18
18
  var serverHomePreferences = require("./server-home-preferences");
19
19
  var serverDefaultAi = require("./server-default-ai");
20
+ var serverDefaultVendor = require("./server-default-vendor");
21
+ var serverContextView = require("./server-context-view");
20
22
  var serverCursorSharing = require("./server-cursor-sharing");
21
23
  var serverTools = require("./server-tools");
22
24
  var serverAdmin = require("./server-admin");
@@ -943,9 +945,9 @@ function createServer(opts) {
943
945
  return;
944
946
  }
945
947
 
946
- // Auth check for project routes
947
- // Bypass auth for MCP bridge endpoint (localhost only).
948
- // The mcp-bridge-server.js runs as a local child process and cannot carry cookies.
948
+ // Auth check for project routes. The local MCP bridge gets a narrowly
949
+ // scoped project capability from its parent process; localhost alone is
950
+ // not sufficient because browser/local callers must not reach the bridge.
949
951
  var projectUrlForAuth = stripPrefix(req.url.split("?")[0], slug);
950
952
  // req.socket.remoteAddress may differ between HTTP/HTTPS (TLSSocket wraps net.Socket).
951
953
  // Also check req.connection.remoteAddress for compatibility.
@@ -956,10 +958,18 @@ function createServer(opts) {
956
958
  var isMcpBridgeLocal = projectUrlForAuth === "/api/mcp-bridge"
957
959
  && req.method === "POST"
958
960
  && isLocalhost;
961
+ var isMcpBridgeAuthenticated = isMcpBridgeLocal
962
+ && typeof ctx.isMcpBridgeAuthenticated === "function"
963
+ && ctx.isMcpBridgeAuthenticated(req);
959
964
  if (projectUrlForAuth === "/api/mcp-bridge") {
960
- console.log("[server] MCP bridge auth: method=" + req.method + " addr=" + remoteAddr + " bypass=" + isMcpBridgeLocal);
965
+ console.log("[server] MCP bridge auth: method=" + req.method + " addr=" + remoteAddr + " capability=" + isMcpBridgeAuthenticated);
961
966
  }
962
- if (!isMcpBridgeLocal && !isRequestAuthed(req)) {
967
+ if (projectUrlForAuth === "/api/mcp-bridge" && !isMcpBridgeAuthenticated) {
968
+ res.writeHead(403, { "Content-Type": "application/json" });
969
+ res.end('{"error":"MCP bridge capability required"}');
970
+ return;
971
+ }
972
+ if (!isMcpBridgeAuthenticated && !isRequestAuthed(req)) {
963
973
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
964
974
  res.end(auth.getAuthPage());
965
975
  return;
@@ -969,7 +979,7 @@ function createServer(opts) {
969
979
  res.setHeader("Set-Cookie", "clay_last_project=" + slug + "; Path=/; SameSite=Strict; Max-Age=31536000" + (tlsOptions ? "; Secure" : ""));
970
980
 
971
981
  // Multi-user: check project access for HTTP requests
972
- if (users.isMultiUser()) {
982
+ if (users.isMultiUser() && !isMcpBridgeAuthenticated) {
973
983
  var httpUser = getMultiUserFromReq(req);
974
984
  if (!httpUser || !canUserAccessSlug(httpUser.id, slug)) {
975
985
  res.writeHead(302, { "Location": "/" });
@@ -1067,6 +1077,8 @@ function createServer(opts) {
1067
1077
  var wss = new WebSocketServer({ noServer: true });
1068
1078
  var homePreferencesHandler = null;
1069
1079
  var defaultAiService = null;
1080
+ var defaultVendorService = null;
1081
+ var contextViewService = null;
1070
1082
  var cursorSharingService = null;
1071
1083
 
1072
1084
  // Slug-less /ws handler: lets a user with no projects yet load the regular
@@ -1321,6 +1333,7 @@ function createServer(opts) {
1321
1333
  projectOwnerId: projectOwnerId || null,
1322
1334
  projectKnowledgeId: extra.projectKnowledgeId || (worktreeMeta && worktreeMeta.projectKnowledgeId) || null,
1323
1335
  worktreeMeta: worktreeMeta || null,
1336
+ mcpBridgeToken: extra.mcpBridgeToken || null,
1324
1337
  isMate: extra.isMate || false,
1325
1338
  mateId: extra.mateId || (extra.isMate ? path.basename(cwd) : null),
1326
1339
  mateDisplayName: extra.mateDisplayName || "",
@@ -1552,6 +1565,8 @@ function createServer(opts) {
1552
1565
  });
1553
1566
 
1554
1567
  defaultAiService = serverDefaultAi.attachDefaultAiService({ users: users, homeChatHandler: homeChatHandler, forEachAppClient: forEachAppClient });
1568
+ defaultVendorService = serverDefaultVendor.attachDefaultVendorService({ users: users, homeChatHandler: homeChatHandler, forEachAppClient: forEachAppClient });
1569
+ contextViewService = serverContextView.attachContextViewService({ users: users, forEachAppClient: forEachAppClient });
1555
1570
  cursorSharingService = serverCursorSharing.attachCursorSharingService({ users: users, forEachAppClient: forEachAppClient });
1556
1571
 
1557
1572
  // --- User-installed home tools handler ---
@@ -1580,6 +1595,8 @@ function createServer(opts) {
1580
1595
 
1581
1596
  function handleDmMessage(ws, msg, routedProjectSlug) {
1582
1597
  if (cursorSharingService && cursorSharingService.handleMessage(ws, msg)) return true;
1598
+ if (defaultVendorService && defaultVendorService.handleMessage(ws, msg, routedProjectSlug)) return true;
1599
+ if (contextViewService && contextViewService.handleMessage(ws, msg)) return true;
1583
1600
  if (defaultAiService && defaultAiService.handleMessage(ws, msg)) return true;
1584
1601
  if (assignmentService.handleMessage(ws, msg, routedProjectSlug)) return true;
1585
1602
  if (dmHandler.handleMessage(ws, msg)) return true;
@@ -4,10 +4,8 @@ var buildShape = require("./session-spawn-mcp-server").buildShape;
4
4
 
5
5
  var MEMORY_CONTRACT =
6
6
  "The sticky-note board persists across sessions and is a user-facing artifact shared with people and Clay agents, not private agent scratch space. " +
7
- "Default to not writing. Create a note proactively only when the user explicitly asks to remember or track something, or when all of these are true: it will remain useful after the current task and session, it is not already adequately recorded in the repository or another note, and the user would likely be glad to find it on the board a week later. " +
8
- "Good notes capture an unresolved commitment, durable product decision, user preference, constraint, or handoff that will materially change future work. " +
9
- "Important exception for deferred defects: while doing code or technical work, actively create a sticky note when you discover a concrete defect, regression risk, security issue, or data-loss risk that is outside the current session goal and will remain unfixed when the turn ends. Do not wait for the user to ask. For a Project Driver session with Issues authority, search, reuse, or create the Issue first and put the primary details there: observable evidence, affected component, impact, and next action. Then make the sticky note only a concise title, one-line actionable cue, and the actual opaque issue: reference; do not duplicate the full defect details in the note. If you lack Issues authority, route through an eligible Project Driver; never invent an issue reference or drop the alert. Check active notes first when a duplicate is plausible. Do not create defect notes for speculation, general cleanup ideas, or problems you fixed in the current session. " +
10
- "Important exception for deferred proposals: when you propose work (a fix, follow-up, improvement, or next step) and the user defers it rather than declining it (\"let's do that later\", \"next time\", \"after this\"), actively create a sticky note before the topic moves on. Do not wait to be asked. Capture what was proposed, why it mattered, and the agreed timing if any. Deferred agreements scroll out of chat history quickly and are hard to track; the board is where they survive. If the user declines the idea outright, do not write a note. " +
7
+ "Default to not writing. Create a note proactively only when the user explicitly asks to remember or track something, or when all of these are true: it will remain useful after the current task and session, it is not already adequately recorded in the repository, an Issue, or another note, and the user would likely be glad to find it on the board a week later. " +
8
+ "Use Sticky Notes for preferences, reminders, undeveloped ideas, or an explicit request to remember something; they are not mandatory duplicates of Project Issues. For concrete actionable bugs, improvements, or deferred implementation, an authorized Project Driver should use Project Issues. If you lack Issues authority, route the work through an eligible Project Driver. Check active notes first when a duplicate is plausible. " +
11
9
  "Never create a note merely because work is important, lengthy, spans agents or restarts, or might help another agent. Do not record completed work, implementation details, test results, investigation logs, transient blockers, conversation summaries, or announcements of your own activity. When uncertain, do not write. " +
12
10
  "Updates are visible too: update only when durable state materially changes. " +
13
11
  "A note is an attention item, so finishing it means closing it, never erasing it: call close_note once the thing it is asking for is done, and the note leaves the active board while the record stays and can be reopened. Do not turn a note into a completion log instead of closing it. " +
@@ -35,7 +33,7 @@ function getToolDefs(handlers) {
35
33
  },
36
34
  {
37
35
  name: "close_note",
38
- description: MEMORY_CONTRACT + " Close a note created by this same session once what it asked for is done. Closing is not deletion: the note leaves the active board, stays queryable under state closed, and can be reopened. Notes created by users or other sessions cannot be closed by this session.",
36
+ description: MEMORY_CONTRACT + " Close a note once what it asked for is done. Authorized Project Drivers may close any note in their project, including notes created by people or other sessions. Other sessions may only close their own notes. Closing is not deletion: the note leaves the active board, stays queryable under state closed, and can be reopened.",
39
37
  inputSchema: buildShape({
40
38
  id: { type: "string", description: "Id of the note to close." },
41
39
  }, ["id"]),
@@ -235,12 +235,12 @@ function attachPairFactory(ctx) {
235
235
  var request = args || {};
236
236
  var driverRuntime = preflightRuntime(request.driver || {}, "claude");
237
237
  var workerRuntime = preflightRuntime(request.worker || {}, null);
238
- var driver = sm.createSessionRaw({ ownerId: ownerId || null, vendor: driverRuntime.vendor, model: driverRuntime.model, effort: driverRuntime.effort });
238
+ var driver = sm.createSessionRaw({ ownerId: ownerId || null, vendor: driverRuntime.vendor, model: driverRuntime.model, effort: driverRuntime.effort, hidden: request.hidden === true });
239
239
  var worker = null;
240
240
  try {
241
241
  driver.title = request.name ? request.name + " · Driver" : "Scheduled task · Driver";
242
242
  if (driverRuntime.explicitEffort) driver.loopSettings = { effort: driverRuntime.effort };
243
- worker = sm.createSessionRaw({ ownerId: ownerId || null, vendor: workerRuntime.vendor, model: workerRuntime.model, effort: workerRuntime.effort });
243
+ worker = sm.createSessionRaw({ ownerId: ownerId || null, vendor: workerRuntime.vendor, model: workerRuntime.model, effort: workerRuntime.effort, hidden: request.hidden === true });
244
244
  worker.title = request.name ? request.name + " · Split Worker" : "Scheduled task · Split Worker";
245
245
  if (workerRuntime.explicitEffort) worker.loopSettings = { effort: workerRuntime.effort };
246
246
  sessionProvenance.markWorker(driver, worker, sm.sessions);
@@ -3,9 +3,10 @@
3
3
  var buildShape = require("./session-spawn-mcp-server").buildShape;
4
4
  var z;
5
5
  try { z = require("zod"); } catch (e) { z = null; }
6
+ var EVALUATION_OUTCOMES = ["succeeded", "partial", "failed", "abandoned"];
6
7
  var evaluationSchema = z ? z.union([
7
- z.string(),
8
- z.object({ outcome: z.string(), note: z.string().optional() }),
8
+ z.enum(EVALUATION_OUTCOMES),
9
+ z.object({ outcome: z.enum(EVALUATION_OUTCOMES), note: z.string().optional() }),
9
10
  ]) : null;
10
11
 
11
12
  // `options.lifecycle` adds the autonomous management tools. They are omitted
@@ -75,7 +76,7 @@ function getToolDefs(handlers, options) {
75
76
  recommendationRationale: { type: "string", description: "Concise Driver-authored explanation of why the recommended replacement vendor, model, and effort fit the next task." },
76
77
  evaluation: {
77
78
  schema: evaluationSchema,
78
- description: "Optional bounded assessment of the Worker being replaced, recorded against that exact generation.",
79
+ description: "Optional assessment of the Worker being replaced. Use succeeded, partial, failed, or abandoned, or an object with outcome set to one of those values and an optional note. Omit when no assessment is needed.",
79
80
  },
80
81
  operationId: { type: "string", description: "Optional stable id for this replacement. Reusing it within the same human turn returns the original operation instead of replacing twice." },
81
82
  supersedeProposalId: { type: "string", description: "Exact pending replacement proposal id to supersede with this new configuration." },
@@ -86,7 +87,7 @@ function getToolDefs(handlers, options) {
86
87
  name: "record_partner_evaluation",
87
88
  description: "Record a bounded assessment of one Split Worker generation so your later model choices can use observed outcomes instead of re-reading transcripts. Records your judgement alongside the objective signals Clay already measured (turns, errors, interruption, context used). This is scoped to your own pair and forms no global model ranking.",
88
89
  inputSchema: buildShape({
89
- outcome: { type: "string", description: 'One of "succeeded", "partial", "failed", "abandoned".' },
90
+ outcome: { enum: EVALUATION_OUTCOMES, description: 'One of "succeeded", "partial", "failed", "abandoned".' },
90
91
  note: { type: "string", description: "Optional short reason, up to 400 characters." },
91
92
  generation: { type: "number", description: "Optional generation number. Defaults to the current Worker." },
92
93
  }, ["outcome"]),
@@ -0,0 +1,89 @@
1
+ // Coordinates bounded semantic title generation for completed conversations.
2
+
3
+ var MAX_ATTEMPTS = 3;
4
+
5
+ function isWorker(session) {
6
+ return !!(session && session.sessionProvenance && session.sessionProvenance.kind === "worker");
7
+ }
8
+
9
+ function hasUserMessage(session) {
10
+ var history = session && Array.isArray(session.history) ? session.history : [];
11
+ for (var i = 0; i < history.length; i++) {
12
+ if (history[i] && history[i].type === "user_message" && typeof history[i].text === "string" && history[i].text.trim()) return true;
13
+ }
14
+ return false;
15
+ }
16
+
17
+ function attachSessionTitleGenerator(ctx) {
18
+ var sm = ctx.sm;
19
+ var getAdapter = ctx.getAdapter;
20
+ var getLinuxUser = ctx.getLinuxUser || function () { return null; };
21
+ var getRuntimeEnv = ctx.getRuntimeEnv || function () { return process.env; };
22
+ var cwd = ctx.cwd;
23
+
24
+ function isLive(session) {
25
+ if (!session || session.destroying || isWorker(session)) return false;
26
+ if (!sm || !sm.sessions || typeof sm.sessions.get !== "function") return true;
27
+ return sm.sessions.get(session.localId) === session;
28
+ }
29
+
30
+ function messagesFor(session) {
31
+ var result = [];
32
+ var history = Array.isArray(session.history) ? session.history : [];
33
+ for (var i = 0; i < history.length; i++) {
34
+ var entry = history[i];
35
+ if (entry && entry.type === "user_message" && entry.text) {
36
+ result.push(entry.text.substring(0, 200));
37
+ if (result.length >= 5) break;
38
+ }
39
+ }
40
+ return result;
41
+ }
42
+
43
+ function generate(session) {
44
+ if (!isLive(session) || session.titleManuallySet || session.titleAutoGenerated || !hasUserMessage(session)) return false;
45
+ if (session._autoTitleInFlight || (session._autoTitleAttempts || 0) >= MAX_ATTEMPTS) return false;
46
+ var sessionAdapter = getAdapter(session);
47
+ if (!sessionAdapter || typeof sessionAdapter.generateTitle !== "function") return false;
48
+ var messages = messagesFor(session);
49
+ if (!messages.length) return false;
50
+
51
+ session._autoTitleAttempts = (session._autoTitleAttempts || 0) + 1;
52
+ session._autoTitleInFlight = true;
53
+ var requestTitle = session.title;
54
+ return Promise.resolve().then(function () {
55
+ return sessionAdapter.generateTitle(messages, {
56
+ cwd: cwd,
57
+ linuxUser: getLinuxUser(session) || undefined,
58
+ env: getRuntimeEnv(session),
59
+ });
60
+ }).then(function (title) {
61
+ title = typeof title === "string" ? title.trim() : "";
62
+ if (title.length < 2) return false;
63
+ title = title.substring(0, 100);
64
+ if (!isLive(session) || session.titleManuallySet || session.titleAutoGenerated || session.title !== requestTitle) return false;
65
+ session.title = title;
66
+ session.titleProvisional = false;
67
+ session.titleAutoGenerated = true;
68
+ sm.saveSessionFile(session);
69
+ sm.broadcastSessionList();
70
+ if (session.cliSessionId && typeof sessionAdapter.renameSession === "function") {
71
+ return Promise.resolve(sessionAdapter.renameSession(session.cliSessionId, title, { dir: cwd })).then(function () { return true; });
72
+ }
73
+ return true;
74
+ }).catch(function (e) {
75
+ console.error("[auto-title] Failed:", e.message || e);
76
+ return false;
77
+ }).then(function (result) {
78
+ session._autoTitleInFlight = false;
79
+ return result;
80
+ }, function (e) {
81
+ session._autoTitleInFlight = false;
82
+ throw e;
83
+ });
84
+ }
85
+
86
+ return { generate: generate, maxAttempts: MAX_ATTEMPTS };
87
+ }
88
+
89
+ module.exports = { attachSessionTitleGenerator: attachSessionTitleGenerator };
@@ -2,7 +2,7 @@
2
2
  // Initial titles are immediate, provisional UI labels. Model-generated titles
3
3
  // replace them after enough conversation exists to describe the actual topic.
4
4
 
5
- var DEFAULT_GENERATION_TURN = 2;
5
+ var DEFAULT_GENERATION_TURN = 1;
6
6
  var HOME_GENERATION_TURN = 1;
7
7
  var MAX_PROVISIONAL_CHARS = 60;
8
8
  var LEGACY_SEARCH_TITLE = "This conversation began in Clay Studio's global search.";
@@ -25,6 +25,7 @@ function firstRecordedUserMessage(session) {
25
25
 
26
26
  function selectInitialTitle(session, transportText) {
27
27
  var existing = cleanText(session && session.title);
28
+ if (existing && session && session.titleProvisional === true) return { title: existing, provisional: true };
28
29
  if (existing) return { title: existing, provisional: false };
29
30
 
30
31
  // Prefer the visible message recorded in history. Query transports may wrap
@@ -41,9 +42,13 @@ function generationTurn(isMate) {
41
42
  return isMate === true ? HOME_GENERATION_TURN : DEFAULT_GENERATION_TURN;
42
43
  }
43
44
 
45
+ function isLegacySearchTitle(value) {
46
+ return cleanText(value).indexOf(LEGACY_SEARCH_TITLE) === 0;
47
+ }
48
+
44
49
  function repairLegacySearchTitle(session) {
45
50
  var title = cleanText(session && session.title);
46
- if (!session || title.indexOf(LEGACY_SEARCH_TITLE) !== 0) return false;
51
+ if (!session || !isLegacySearchTitle(title)) return false;
47
52
  var source = firstRecordedUserMessage(session);
48
53
  if (!source) return false;
49
54
  session.title = source.length > MAX_PROVISIONAL_CHARS ? source.substring(0, MAX_PROVISIONAL_CHARS) : source;
@@ -56,5 +61,6 @@ module.exports = {
56
61
  HOME_GENERATION_TURN: HOME_GENERATION_TURN,
57
62
  selectInitialTitle: selectInitialTitle,
58
63
  generationTurn: generationTurn,
64
+ isLegacySearchTitle: isLegacySearchTitle,
59
65
  repairLegacySearchTitle: repairLegacySearchTitle,
60
66
  };
package/lib/sessions.js CHANGED
@@ -8,6 +8,7 @@ var hygiene = require("./session-hygiene");
8
8
  var backgroundTaskTiming = require("./background-task-timing");
9
9
  var sessionProvenance = require("./session-provenance");
10
10
  var sessionVisibility = require("./session-visibility");
11
+ var sessionTitlePolicy = require("./session-title-policy");
11
12
  var projectAutonomousRun = require("./project-autonomous-run").projectAutonomousRun;
12
13
  var restoreStoredRun = require("./project-autonomous-run").restoreStoredRun;
13
14
 
@@ -23,6 +24,7 @@ function createSessionManager(opts) {
23
24
  var sessionViewedListeners = [];
24
25
  var onSessionRenamed = opts.onSessionRenamed || function () {};
25
26
  var onSessionIdentityAssigned = opts.onSessionIdentityAssigned || function () {};
27
+ var onSessionHydrate = opts.onSessionHydrate || null;
26
28
 
27
29
  // --- Multi-session state ---
28
30
  var nextLocalId = 1;
@@ -153,11 +155,15 @@ function createSessionManager(opts) {
153
155
  localId: session.localId,
154
156
  cliSessionId: session.cliSessionId,
155
157
  title: session.title,
158
+ titleProvisional: session.titleProvisional === true,
159
+ titleAutoGenerated: session.titleAutoGenerated === true,
160
+ titleManuallySet: session.titleManuallySet === true,
156
161
  createdAt: session.createdAt,
157
162
  };
158
163
  if (session.ownerId) metaObj.ownerId = session.ownerId;
159
164
  if (session.vendor) metaObj.vendor = session.vendor;
160
165
  if (session.codexDynamicToolCatalogVersion) metaObj.codexDynamicToolCatalogVersion = session.codexDynamicToolCatalogVersion;
166
+ if (session.codexIssuesToolCatalogVersion) metaObj.codexIssuesToolCatalogVersion = session.codexIssuesToolCatalogVersion;
161
167
  if (session.model) metaObj.model = session.model;
162
168
  if (session.effort) metaObj.effort = session.effort;
163
169
  // Persist the session's "born" mode so TUI sessions reappear after a
@@ -177,6 +183,7 @@ function createSessionManager(opts) {
177
183
  if (session.loopInterviewHandoff) metaObj.loopInterviewHandoff = session.loopInterviewHandoff;
178
184
  if (session.scheduledTaskDraft) metaObj.scheduledTaskDraft = session.scheduledTaskDraft;
179
185
  if (session.scheduledTaskRun) metaObj.scheduledTaskRun = session.scheduledTaskRun;
186
+ if (session.hidden) metaObj.hidden = true;
180
187
  metaObj.sessionVisibility = sessionVisibility.normalize(session.sessionVisibility);
181
188
  if (session.sessionVisibilityExplicit) metaObj.sessionVisibilityExplicit = true;
182
189
  if (session.bookmarked) metaObj.bookmarked = true;
@@ -285,6 +292,11 @@ function createSessionManager(opts) {
285
292
  messageUUIDs.push({ uuid: loaded[i].history[k].uuid, type: loaded[i].history[k].messageType, historyIndex: k });
286
293
  }
287
294
  }
295
+ var hasTitleProvenance = Object.prototype.hasOwnProperty.call(m, "titleProvisional")
296
+ || Object.prototype.hasOwnProperty.call(m, "titleAutoGenerated")
297
+ || Object.prototype.hasOwnProperty.call(m, "titleManuallySet");
298
+ var legacyTitleProtected = !hasTitleProvenance && typeof m.title === "string"
299
+ && m.title.trim() && !sessionTitlePolicy.isLegacySearchTitle(m.title);
288
300
  var session = {
289
301
  localId: localId,
290
302
  queryInstance: null,
@@ -296,6 +308,9 @@ function createSessionManager(opts) {
296
308
  pendingAskUser: {},
297
309
  isProcessing: false,
298
310
  title: m.title || "",
311
+ titleProvisional: m.titleProvisional === true,
312
+ titleAutoGenerated: m.titleAutoGenerated === true,
313
+ titleManuallySet: m.titleManuallySet === true || legacyTitleProtected,
299
314
  createdAt: m.createdAt || Date.now(),
300
315
  lastActivity: loaded[i].mtime || m.createdAt || Date.now(),
301
316
  history: loaded[i].history,
@@ -304,6 +319,7 @@ function createSessionManager(opts) {
304
319
  };
305
320
  if (m.vendor) session.vendor = m.vendor;
306
321
  session.codexDynamicToolCatalogVersion = Number(m.codexDynamicToolCatalogVersion || 0);
322
+ session.codexIssuesToolCatalogVersion = Number(m.codexIssuesToolCatalogVersion || 0);
307
323
  session.model = m.model || deriveSessionModel(loaded[i].history);
308
324
  session.effort = m.effort || null;
309
325
  if (m.loop) session.loop = m.loop;
@@ -338,6 +354,7 @@ function createSessionManager(opts) {
338
354
  session.loopInterviewHandoff = m.loopInterviewHandoff && typeof m.loopInterviewHandoff === "object" ? m.loopInterviewHandoff : null;
339
355
  session.scheduledTaskDraft = m.scheduledTaskDraft && typeof m.scheduledTaskDraft === "object" ? m.scheduledTaskDraft : null;
340
356
  session.scheduledTaskRun = m.scheduledTaskRun && typeof m.scheduledTaskRun === "object" ? m.scheduledTaskRun : null;
357
+ session.hidden = m.hidden === true || !!session.scheduledTaskRun;
341
358
  session.terminalId = null;
342
359
  session.runtimeMode = null;
343
360
  session.runtimeTerminalId = null;
@@ -641,6 +658,7 @@ function createSessionManager(opts) {
641
658
  allowedTools: {},
642
659
  isProcessing: false,
643
660
  title: "",
661
+ titleProvisional: false,
644
662
  titleAutoGenerated: false,
645
663
  turnCount: 0,
646
664
  createdAt: Date.now(),
@@ -664,6 +682,7 @@ function createSessionManager(opts) {
664
682
  loopInterviewHandoff: null,
665
683
  scheduledTaskDraft: null,
666
684
  scheduledTaskRun: null,
685
+ hidden: !!(sessionOpts && sessionOpts.hidden),
667
686
  terminalId: null,
668
687
  sessionOriginId: null,
669
688
  };
@@ -688,6 +707,7 @@ function createSessionManager(opts) {
688
707
  allowedTools: {},
689
708
  isProcessing: false,
690
709
  title: "",
710
+ titleProvisional: false,
691
711
  titleAutoGenerated: false,
692
712
  turnCount: 0,
693
713
  createdAt: Date.now(),
@@ -711,6 +731,7 @@ function createSessionManager(opts) {
711
731
  loopInterviewHandoff: null,
712
732
  scheduledTaskDraft: null,
713
733
  scheduledTaskRun: null,
734
+ hidden: !!(sessionOpts && sessionOpts.hidden),
714
735
  terminalId: null,
715
736
  sessionOriginId: null,
716
737
  };
@@ -779,6 +800,7 @@ function createSessionManager(opts) {
779
800
  function switchSession(localId, targetWs, transform) {
780
801
  var session = sessions.get(localId);
781
802
  if (!session) return;
803
+ if (typeof onSessionHydrate === "function") onSessionHydrate(session);
782
804
 
783
805
  // Pane sockets (split-view iframes) pin their session via switch_session
784
806
  // but must not move the project-global active pointer: reloads and
@@ -806,7 +828,9 @@ function createSessionManager(opts) {
806
828
  var _vendorCaps = _capsByVendor[_sessionVendor] || {};
807
829
  _send({ type: "session_switched", id: localId, cliSessionId: session.cliSessionId || null, loop: session.loop || null, vendor: session.vendor || null, model: session.model || null, effort: session.effort || null, hasHistory: (session.history && session.history.length > 0), capabilities: _vendorCaps, isProcessing: !!session.isProcessing, mode: session.mode || "gui", terminalId: typeof session.terminalId === "number" ? session.terminalId : null, runtimeMode: session.runtimeMode || null, runtimeTerminalId: typeof session.runtimeTerminalId === "number" ? session.runtimeTerminalId : null, tuiSuspended: !!session.tuiSuspended, dangerouslySkipPermissions: !!session.dangerouslySkipPermissions, permissionMode: session.permissionMode || null, autonomousRun: projectAutonomousRun(session.autonomousRun), loopInterviewBrief: session.loopInterviewBrief || null, loopInterviewHandoff: session.loopInterviewHandoff || null, scheduledTaskDraft: session.scheduledTaskDraft || null });
808
830
  // Send vendor-specific slash commands
809
- var _vendorCmds = slashCommandsByVendor[_sessionVendor] || slashCommands || [];
831
+ var _vendorCmds = (session.slashCommandsByVendor && session.slashCommandsByVendor[_sessionVendor])
832
+ || session.slashCommands
833
+ || [];
810
834
  _send({ type: "slash_commands", commands: _vendorCmds, vendor: _sessionVendor });
811
835
  broadcastSessionList();
812
836
  replayHistory(session, undefined, targetWs, transform);
@@ -1270,6 +1294,7 @@ function createSessionManager(opts) {
1270
1294
  }
1271
1295
 
1272
1296
  return {
1297
+ setSessionHydrator: function (fn) { onSessionHydrate = typeof fn === "function" ? fn : null; },
1273
1298
  get activeSessionId() { return activeSessionId; },
1274
1299
  get nextLocalId() { return nextLocalId; },
1275
1300
  get slashCommands() { return slashCommands; },
@@ -12,6 +12,11 @@ function sessionIdForPersistence(session) {
12
12
  return session.cliSessionId || session.localId || null;
13
13
  }
14
14
 
15
+ function shouldPersistDisconnectPresence(existing, sessionId, existingSession, currentSession) {
16
+ return !!sessionId && (!existing || existing.sessionId === sessionId ||
17
+ (!!existingSession && !!currentSession && existingSession === currentSession));
18
+ }
19
+
15
20
  function findSession(sessions, sessionId) {
16
21
  if (!sessions || sessionId == null) return null;
17
22
  var found = null;
@@ -100,6 +105,7 @@ load();
100
105
 
101
106
  module.exports = {
102
107
  sessionIdForPersistence: sessionIdForPersistence,
108
+ shouldPersistDisconnectPresence: shouldPersistDisconnectPresence,
103
109
  findSession: findSession,
104
110
  setPresence: setPresence,
105
111
  getPresence: getPresence,
@@ -0,0 +1,65 @@
1
+ // Durable user-scoped context display preference storage.
2
+
3
+ var PREFERENCE_KEY = "contextViewPreference";
4
+ var MODES = ["off", "mini", "panel"];
5
+
6
+ function validMode(value) {
7
+ return MODES.indexOf(value) !== -1;
8
+ }
9
+
10
+ function attachContextViewPreferences(deps) {
11
+ var loadUsers = deps.loadUsers;
12
+ var saveUsers = deps.saveUsers;
13
+ var isMultiUser = deps.isMultiUser || function () { return false; };
14
+ var configModule = deps.configModule || require("./config");
15
+
16
+ function findRecord(data, userId) {
17
+ var records = Array.isArray(data.users) ? data.users : [];
18
+ for (var i = 0; i < records.length; i++) if (records[i] && records[i].id === userId) return records[i];
19
+ return null;
20
+ }
21
+
22
+ function result(value, source) {
23
+ if (value === undefined) return { present: false, mode: "off", source: null };
24
+ if (!validMode(value)) return { present: true, mode: "off", source: source, error: "Context display preference is invalid." };
25
+ return { present: true, mode: value, source: source };
26
+ }
27
+
28
+ function get(userId) {
29
+ var data;
30
+ try { data = loadUsers(); } catch (error) { return { present: false, mode: "off", source: null, error: "Unable to read context display preference: " + (error.message || error) }; }
31
+ var record = findRecord(data, userId);
32
+ if (isMultiUser() && !record) return { present: false, mode: "off", source: null, error: "User not found." };
33
+ if (record && Object.prototype.hasOwnProperty.call(record, PREFERENCE_KEY)) return result(record[PREFERENCE_KEY], "explicit");
34
+ if (record) return { present: false, mode: "off", source: null };
35
+ if (userId === "default") {
36
+ var config;
37
+ try { config = configModule.loadConfig() || {}; } catch (configError) { return { present: false, mode: "off", source: null, error: "Unable to read context display preference: " + (configError.message || configError) }; }
38
+ if (Object.prototype.hasOwnProperty.call(config, PREFERENCE_KEY)) return result(config[PREFERENCE_KEY], "explicit");
39
+ }
40
+ return { present: false, mode: "off", source: null };
41
+ }
42
+
43
+ function set(userId, mode) {
44
+ if (!validMode(mode)) return { ok: false, error: "Context display preference is invalid." };
45
+ var data;
46
+ try { data = loadUsers(); } catch (error) { return { ok: false, error: "Unable to read context display preference: " + (error.message || error) }; }
47
+ var record = findRecord(data, userId);
48
+ if (isMultiUser() && !record) return { ok: false, error: "User not found." };
49
+ if (record) {
50
+ var nextData = Object.assign({}, data, { users: (data.users || []).map(function (item) { return item === record ? Object.assign({}, item, { contextViewPreference: mode }) : item; }) });
51
+ try { saveUsers(nextData); } catch (saveError) { return { ok: false, error: "Unable to save context display preference: " + (saveError.message || saveError) }; }
52
+ return { ok: true, mode: mode };
53
+ }
54
+ if (userId !== "default") return { ok: false, error: "User not found." };
55
+ try {
56
+ var current = configModule.loadConfig() || {};
57
+ configModule.saveConfig(Object.assign({}, current, { contextViewPreference: mode }));
58
+ return { ok: true, mode: mode };
59
+ } catch (configSaveError) { return { ok: false, error: "Unable to save context display preference: " + (configSaveError.message || configSaveError) }; }
60
+ }
61
+
62
+ return { get: get, set: set, validMode: validMode, MODES: MODES };
63
+ }
64
+
65
+ module.exports = { attachContextViewPreferences: attachContextViewPreferences, validMode: validMode, MODES: MODES, PREFERENCE_KEY: PREFERENCE_KEY };