clay-server 3.8.1 → 4.0.0-beta.2

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 (189) hide show
  1. package/lib/ask-user-mcp-server.js +5 -119
  2. package/lib/builtin-mates.js +6 -6
  3. package/lib/claude-hook-installer.js +15 -0
  4. package/lib/clay-history-mcp-server.js +62 -297
  5. package/lib/daemon.js +1 -1
  6. package/lib/debate-model-selection.js +119 -0
  7. package/lib/home-debate-tool-policy.js +164 -0
  8. package/lib/home-response-notifications.js +26 -0
  9. package/lib/mate-creation-mcp-server.js +55 -0
  10. package/lib/mate-ready-creation.js +95 -0
  11. package/lib/mates.js +2 -0
  12. package/lib/notes.js +2 -1
  13. package/lib/project-ask-user.js +64 -0
  14. package/lib/project-capsule-catalog.js +101 -0
  15. package/lib/project-connection.js +4 -0
  16. package/lib/project-debate-proposal.js +133 -17
  17. package/lib/project-debate.js +214 -110
  18. package/lib/project-delegated-follow-up.js +84 -0
  19. package/lib/project-delegated-session.js +53 -0
  20. package/lib/project-filesystem.js +7 -9
  21. package/lib/project-knowledge.js +1 -0
  22. package/lib/project-mate-creation-proposal.js +120 -0
  23. package/lib/project-memory.js +14 -7
  24. package/lib/project-models.js +157 -32
  25. package/lib/project-session-notes.js +2 -2
  26. package/lib/project-session-pair.js +8 -8
  27. package/lib/project-sessions.js +66 -51
  28. package/lib/project-vendor-login.js +397 -0
  29. package/lib/project-worker-proposal.js +25 -25
  30. package/lib/project-workspace-query.js +144 -0
  31. package/lib/project.js +349 -102
  32. package/lib/public/app.js +85 -42
  33. package/lib/public/css/avatar-imprints.css +0 -1
  34. package/lib/public/css/base.css +4 -0
  35. package/lib/public/css/capsule-ui.css +389 -0
  36. package/lib/public/css/command-palette.css +306 -6
  37. package/lib/public/css/home-capsule-library.css +243 -0
  38. package/lib/public/css/home-capsule-source.css +192 -0
  39. package/lib/public/css/home-debate-live.css +146 -0
  40. package/lib/public/css/home-debate-planning.css +125 -0
  41. package/lib/public/css/home-debates-archive.css +281 -0
  42. package/lib/public/css/home-hub.css +896 -405
  43. package/lib/public/css/home-mate-creation.css +34 -0
  44. package/lib/public/css/home-mate-model-picker.css +131 -0
  45. package/lib/public/css/home-mate-settings.css +344 -0
  46. package/lib/public/css/home-session-actions.css +265 -0
  47. package/lib/public/css/home-sidebar.css +667 -0
  48. package/lib/public/css/icon-strip.css +1 -2
  49. package/lib/public/css/mates.css +7 -34
  50. package/lib/public/css/pane.css +1 -1
  51. package/lib/public/css/playbook.css +0 -80
  52. package/lib/public/css/sticky-notes.css +53 -117
  53. package/lib/public/css/tui-attention.css +0 -44
  54. package/lib/public/css/workspace-assignment.css +45 -0
  55. package/lib/public/fonts/source-serif-4/LICENSE.md +93 -0
  56. package/lib/public/fonts/source-serif-4/SourceSerif4Caption-Semibold.ttf.woff2 +0 -0
  57. package/lib/public/index.html +93 -74
  58. package/lib/public/modules/app-connection.js +10 -1
  59. package/lib/public/modules/app-dm.js +43 -160
  60. package/lib/public/modules/app-home-hub.js +328 -585
  61. package/lib/public/modules/app-message-cards.js +242 -0
  62. package/lib/public/modules/app-message-router.js +84 -0
  63. package/lib/public/modules/app-messages.js +102 -37
  64. package/lib/public/modules/app-notifications.js +8 -119
  65. package/lib/public/modules/app-projects.js +9 -1
  66. package/lib/public/modules/app-rendering.js +21 -602
  67. package/lib/public/modules/avatar-imprint.js +11 -8
  68. package/lib/public/modules/chat-bubble-renderer.js +152 -0
  69. package/lib/public/modules/chat-render-runtime.js +198 -0
  70. package/lib/public/modules/command-palette.js +225 -436
  71. package/lib/public/modules/dm-render.js +82 -0
  72. package/lib/public/modules/home-capsule-creation-intent.js +31 -0
  73. package/lib/public/modules/home-capsule-library.js +146 -0
  74. package/lib/public/modules/home-capsule-source.js +383 -0
  75. package/lib/public/modules/home-chat-identity.js +35 -0
  76. package/lib/public/modules/home-chat-scroll.js +77 -0
  77. package/lib/public/modules/home-chat-stream-state.js +34 -0
  78. package/lib/public/modules/home-composer-focus.js +25 -0
  79. package/lib/public/modules/home-conversations-sheet.js +179 -0
  80. package/lib/public/modules/home-debate-activity.js +48 -0
  81. package/lib/public/modules/home-debate-controls.js +295 -0
  82. package/lib/public/modules/home-debate-launch.js +41 -0
  83. package/lib/public/modules/home-debate-live.js +284 -0
  84. package/lib/public/modules/home-debate-models.js +73 -0
  85. package/lib/public/modules/home-debate-planning.js +335 -0
  86. package/lib/public/modules/home-debates-archive.js +279 -0
  87. package/lib/public/modules/home-dock-resize.js +74 -0
  88. package/lib/public/modules/home-dock.js +446 -0
  89. package/lib/public/modules/home-mate-chat.js +496 -0
  90. package/lib/public/modules/home-mate-creation.js +109 -0
  91. package/lib/public/modules/home-mate-model-picker.js +281 -0
  92. package/lib/public/modules/home-mate-selection.js +38 -0
  93. package/lib/public/modules/home-mate-settings-menu.js +131 -0
  94. package/lib/public/modules/home-mate-settings.js +383 -0
  95. package/lib/public/modules/home-session-actions.js +373 -0
  96. package/lib/public/modules/home-session-model-confirmation.js +13 -0
  97. package/lib/public/modules/home-shell.js +9 -0
  98. package/lib/public/modules/home-sidebar-chat-list.js +63 -0
  99. package/lib/public/modules/home-sidebar.js +158 -0
  100. package/lib/public/modules/home-sub-surface.js +19 -0
  101. package/lib/public/modules/home-surface-boot.js +68 -0
  102. package/lib/public/modules/home-surface.js +141 -0
  103. package/lib/public/modules/home-tools.js +271 -0
  104. package/lib/public/modules/markdown.js +50 -2
  105. package/lib/public/modules/mate-management.js +86 -0
  106. package/lib/public/modules/mate-wizard.js +0 -7
  107. package/lib/public/modules/mention.js +17 -19
  108. package/lib/public/modules/pane-bridge.js +9 -0
  109. package/lib/public/modules/profile.js +4 -5
  110. package/lib/public/modules/project-settings.js +34 -28
  111. package/lib/public/modules/project-switcher.js +11 -5
  112. package/lib/public/modules/project-workspace-assignment.js +23 -0
  113. package/lib/public/modules/search-clay-chat.js +402 -0
  114. package/lib/public/modules/server-settings.js +14 -12
  115. package/lib/public/modules/session-actions.js +1 -1
  116. package/lib/public/modules/sidebar-mates.js +56 -552
  117. package/lib/public/modules/sidebar-mobile.js +7 -64
  118. package/lib/public/modules/sidebar-presence.js +37 -0
  119. package/lib/public/modules/split-pair-ui.js +13 -13
  120. package/lib/public/modules/split-view.js +7 -0
  121. package/lib/public/modules/sticky-notes.js +7 -6
  122. package/lib/public/modules/tool-input-composition.js +48 -0
  123. package/lib/public/modules/tool-llm-status.js +167 -0
  124. package/lib/public/modules/tool-renderer-advanced.js +285 -0
  125. package/lib/public/modules/tool-renderer-chart.js +158 -0
  126. package/lib/public/modules/tool-renderer-semantics.js +35 -0
  127. package/lib/public/modules/tool-renderer.js +464 -0
  128. package/lib/public/modules/tool-runtime.js +309 -0
  129. package/lib/public/modules/tool-ui-evaluator.js +97 -0
  130. package/lib/public/modules/tool-ui-tree.js +21 -0
  131. package/lib/public/modules/tui-attention.js +10 -0
  132. package/lib/public/modules/user-settings.js +16 -23
  133. package/lib/public/modules/vendor-login.js +287 -0
  134. package/lib/public/modules/worker-proposal.js +2 -2
  135. package/lib/public/modules/workspace-assignment-card.js +149 -0
  136. package/lib/public/modules/workspace-assignment-routing.js +4 -0
  137. package/lib/public/style.css +13 -2
  138. package/lib/runtime-env.js +65 -0
  139. package/lib/sdk-bridge.js +120 -32
  140. package/lib/sdk-message-processor.js +10 -6
  141. package/lib/server-home-capsule-creation.js +36 -0
  142. package/lib/server-home-chat-events.js +287 -0
  143. package/lib/server-home-chat.js +518 -0
  144. package/lib/server-home-clay-entry.js +65 -0
  145. package/lib/server-home-clay-session-links.js +26 -0
  146. package/lib/server-home-debate-planning.js +214 -0
  147. package/lib/server-home-debates.js +137 -0
  148. package/lib/server-home-mate-creation.js +261 -0
  149. package/lib/server-home-models.js +241 -0
  150. package/lib/server-home-preferences.js +61 -0
  151. package/lib/server-mates.js +37 -0
  152. package/lib/server-tools.js +377 -0
  153. package/lib/server.js +111 -59
  154. package/lib/session-pair-mcp-server.js +6 -6
  155. package/lib/sessions.js +18 -3
  156. package/lib/tool-capsule-source.js +142 -0
  157. package/lib/tool-control-mcp-server.js +126 -0
  158. package/lib/tool-llm.js +48 -0
  159. package/lib/tool-storage.js +77 -0
  160. package/lib/tool-ui-spec-advanced.js +82 -0
  161. package/lib/tool-ui-spec.js +261 -0
  162. package/lib/tools-registry.js +286 -0
  163. package/lib/users-home-dock-preferences.js +77 -0
  164. package/lib/users-home-surface-preferences.js +99 -0
  165. package/lib/users-preferences.js +4 -7
  166. package/lib/users.js +12 -0
  167. package/lib/whats-new-content.js +3 -3
  168. package/lib/workspace-assignment-service.js +420 -0
  169. package/lib/workspace-query-mcp-server.js +154 -0
  170. package/lib/workspace-query-service.js +492 -0
  171. package/lib/ws-schema.js +93 -12
  172. package/lib/yoke/adapters/acp.js +2 -1
  173. package/lib/yoke/adapters/antigravity.js +2 -1
  174. package/lib/yoke/adapters/claude-worker.js +11 -4
  175. package/lib/yoke/adapters/claude.js +73 -43
  176. package/lib/yoke/adapters/codex.js +40 -10
  177. package/lib/yoke/adapters/kiro.js +2 -1
  178. package/lib/yoke/claude-user-input.js +38 -0
  179. package/lib/yoke/codex-app-server.js +20 -2
  180. package/lib/yoke/codex-user-input.js +64 -0
  181. package/lib/yoke/complete-once.js +164 -0
  182. package/lib/yoke/index.js +5 -1
  183. package/lib/yoke/interface.js +19 -0
  184. package/lib/yoke/mcp-bridge-server.js +2 -6
  185. package/lib/yoke/user-input.js +254 -0
  186. package/package.json +3 -2
  187. package/lib/public/css/home-chat.css +0 -380
  188. package/lib/public/modules/home-chat.js +0 -438
  189. package/lib/server-clay-home.js +0 -245
@@ -0,0 +1,99 @@
1
+ // Per-user persistence for durable Home conversation selections.
2
+
3
+ function attachHomeSurfacePreferences(deps) {
4
+ var loadUsers = deps.loadUsers;
5
+ var saveUsers = deps.saveUsers;
6
+
7
+ function defaults() {
8
+ return { surface: null, projectSlug: null, activeMateId: null, activeSessionByMate: {}, sidebarCollapsed: false, chatScope: "all", subSurface: "chat" };
9
+ }
10
+
11
+ function validId(value) {
12
+ return typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(value);
13
+ }
14
+
15
+ function validSessionReference(value) {
16
+ return typeof value === "string" && value.length > 0 && value.length <= 256 && !/[\x00-\x1f]/.test(value);
17
+ }
18
+
19
+ function validProjectSlug(value) {
20
+ return typeof value === "string" && /^[a-z0-9][a-z0-9_-]{0,127}$/.test(value);
21
+ }
22
+
23
+ function normalize(value) {
24
+ var source = value && typeof value === "object" ? value : {};
25
+ var sessions = source.activeSessionByMate && typeof source.activeSessionByMate === "object" && !Array.isArray(source.activeSessionByMate)
26
+ ? source.activeSessionByMate
27
+ : {};
28
+ var normalizedSessions = {};
29
+ var mateIds = Object.keys(sessions);
30
+ for (var i = 0; i < mateIds.length; i++) {
31
+ if (!validId(mateIds[i]) || !validSessionReference(sessions[mateIds[i]])) continue;
32
+ normalizedSessions[mateIds[i]] = sessions[mateIds[i]];
33
+ }
34
+ return {
35
+ surface: source.surface === "home" || source.surface === "project" ? source.surface : null,
36
+ projectSlug: validProjectSlug(source.projectSlug) ? source.projectSlug : null,
37
+ activeMateId: validId(source.activeMateId) ? source.activeMateId : null,
38
+ activeSessionByMate: normalizedSessions,
39
+ sidebarCollapsed: source.sidebarCollapsed === true,
40
+ chatScope: source.chatScope === "current" ? "current" : "all",
41
+ subSurface: source.subSurface === "debates" ? "debates" : "chat",
42
+ };
43
+ }
44
+
45
+ function loadSingleUserPreference() {
46
+ try {
47
+ var config = require("./config");
48
+ var current = config.loadConfig() || {};
49
+ return normalize(current.homeSurfacePreference);
50
+ } catch (e) {
51
+ return defaults();
52
+ }
53
+ }
54
+
55
+ function saveSingleUserPreference(preference) {
56
+ try {
57
+ var config = require("./config");
58
+ var current = config.loadConfig() || {};
59
+ config.saveConfig(Object.assign({}, current, { homeSurfacePreference: preference }));
60
+ return { ok: true, preference: preference };
61
+ } catch (e) {
62
+ return { error: "Unable to save Home surface preference" };
63
+ }
64
+ }
65
+
66
+ function getHomeSurfacePreference(userId) {
67
+ var data = loadUsers();
68
+ for (var i = 0; i < data.users.length; i++) {
69
+ if (data.users[i].id === userId) return normalize(data.users[i].homeSurfacePreference);
70
+ }
71
+ if (userId === "default") return loadSingleUserPreference();
72
+ return defaults();
73
+ }
74
+
75
+ function setHomeSurfacePreference(userId, patch) {
76
+ var current = getHomeSurfacePreference(userId);
77
+ var merged = Object.assign({}, current, patch || {});
78
+ if (patch && patch.activeSessionByMate && typeof patch.activeSessionByMate === "object") {
79
+ merged.activeSessionByMate = Object.assign({}, current.activeSessionByMate, patch.activeSessionByMate);
80
+ }
81
+ var next = normalize(merged);
82
+ var data = loadUsers();
83
+ for (var i = 0; i < data.users.length; i++) {
84
+ if (data.users[i].id !== userId) continue;
85
+ data.users[i].homeSurfacePreference = next;
86
+ saveUsers(data);
87
+ return { ok: true, preference: next };
88
+ }
89
+ if (userId === "default") return saveSingleUserPreference(next);
90
+ return { error: "User not found" };
91
+ }
92
+
93
+ return {
94
+ getHomeSurfacePreference: getHomeSurfacePreference,
95
+ setHomeSurfacePreference: setHomeSurfacePreference,
96
+ };
97
+ }
98
+
99
+ module.exports = { attachHomeSurfacePreferences: attachHomeSurfacePreferences };
@@ -334,14 +334,11 @@ function attachPreferences(deps) {
334
334
  return { error: "User not found" };
335
335
  }
336
336
 
337
- // --- Per-user Mates UI toggle ---
337
+ // --- Per-user project Mate DM shortcut toggle ---
338
338
  //
339
- // When disabled, the entire Mates surface (sidebar avatars, DM "Create a
340
- // Mate" entry, home-hub mates strip) is hidden for this user; multi-user
341
- // and the rest of the sidebar are unaffected. Default is OFF: Mates is
342
- // opt-in, not opt-out. Stored as `matesEnabled`; only the literal `true`
343
- // enables it, so brand-new users (no field set) get the default-off
344
- // experience and must turn Mates on explicitly in settings.
339
+ // `matesEnabled` is retained as the persisted field for compatibility.
340
+ // It now controls only project-local Mate DM shortcuts. The broader Mate
341
+ // capability surface is always available. Default remains off.
345
342
 
346
343
  function getMatesEnabled(userId) {
347
344
  var data = loadUsers();
package/lib/users.js CHANGED
@@ -6,6 +6,8 @@ var { CONFIG_DIR } = require("./config");
6
6
  var { attachAuth } = require("./users-auth");
7
7
  var { DEFAULT_PERMISSIONS, ALL_PERMISSIONS, attachPermissions } = require("./users-permissions");
8
8
  var { attachPreferences } = require("./users-preferences");
9
+ var { attachHomeDockPreferences } = require("./users-home-dock-preferences");
10
+ var { attachHomeSurfacePreferences } = require("./users-home-surface-preferences");
9
11
 
10
12
  var USERS_FILE = path.join(CONFIG_DIR, "users.json");
11
13
 
@@ -394,6 +396,8 @@ function removeExpiredInvites() {
394
396
  var auth = attachAuth({ loadUsers: loadUsers, saveUsers: saveUsers, findAdmin: findAdmin });
395
397
  var permissions = attachPermissions({ loadUsers: loadUsers, saveUsers: saveUsers, findUserById: findUserById });
396
398
  var preferences = attachPreferences({ loadUsers: loadUsers, saveUsers: saveUsers });
399
+ var homeDockPreferences = attachHomeDockPreferences({ loadUsers: loadUsers, saveUsers: saveUsers });
400
+ var homeSurfacePreferences = attachHomeSurfacePreferences({ loadUsers: loadUsers, saveUsers: saveUsers });
397
401
 
398
402
  // Alias auth functions
399
403
  var isMultiUser = auth.isMultiUser;
@@ -442,6 +446,10 @@ var markWhatsNewSeen = preferences.markWhatsNewSeen;
442
446
  var getTerminalFont = preferences.getTerminalFont;
443
447
  var setTerminalFont = preferences.setTerminalFont;
444
448
  var setMateOnboarded = preferences.setMateOnboarded;
449
+ var getHomeDockPreference = homeDockPreferences.getHomeDockPreference;
450
+ var setHomeDockPreference = homeDockPreferences.setHomeDockPreference;
451
+ var getHomeSurfacePreference = homeSurfacePreferences.getHomeSurfacePreference;
452
+ var setHomeSurfacePreference = homeSurfacePreferences.setHomeSurfacePreference;
445
453
 
446
454
  module.exports = {
447
455
  USERS_FILE: USERS_FILE,
@@ -511,6 +519,10 @@ module.exports = {
511
519
  markWhatsNewSeen: markWhatsNewSeen,
512
520
  getTerminalFont: getTerminalFont,
513
521
  setTerminalFont: setTerminalFont,
522
+ getHomeDockPreference: getHomeDockPreference,
523
+ setHomeDockPreference: setHomeDockPreference,
524
+ getHomeSurfacePreference: getHomeSurfacePreference,
525
+ setHomeSurfacePreference: setHomeSurfacePreference,
514
526
  getDeletedBuiltinKeys: getDeletedBuiltinKeys,
515
527
  addDeletedBuiltinKey: addDeletedBuiltinKey,
516
528
  removeDeletedBuiltinKey: removeDeletedBuiltinKey,
@@ -70,13 +70,13 @@ var ENTRIES = [
70
70
  title: "GUI is back as the default",
71
71
  publishedAt: "2026-07-01",
72
72
  image: null,
73
- summary: "Anthropic reversed the June 15 billing split, so SDK usage is no longer billed separately. Clay opens Claude sessions in the GUI chat again by default. Mates is now off by default as it winds down.",
73
+ summary: "Anthropic reversed the June 15 billing split, so SDK usage is no longer billed separately. Clay opens Claude sessions in the GUI chat again by default. Project Mate DM shortcuts are hidden by default.",
74
74
  body: '' +
75
75
  '<p><strong>Good news: Anthropic walked back the June 15 billing change.</strong> The planned Interactive vs. Programmatic split (which would have charged Claude Agent SDK usage at full API rates) is off the table, so the reason we moved everyone to the TUI is gone.</p>' +
76
76
  '<h3>GUI is the default again</h3>' +
77
77
  '<p>New Claude sessions open in the <strong>GUI chat</strong> (SDK) by default, the way they did before. The embedded <code>claude</code> <strong>TUI</strong> is still there whenever you want it &mdash; toggle <strong>"Open Claude as terminal (TUI)"</strong> in Settings.</p>' +
78
- '<h3>Mates is off by default</h3>' +
79
- '<p>As announced, <strong>Mates</strong> is no longer a first-class Clay feature. It is now <strong>off by default</strong> so Clay stays focused as a developer tool. Your existing Mates and their knowledge files remain on disk &mdash; if you still use them, re-enable Mates in <strong>Settings</strong>.</p>' +
78
+ '<h3>Project Mate DM shortcuts are optional</h3>' +
79
+ '<p>Mate DM shortcuts inside project workspaces are hidden by default and can be shown in <strong>Settings</strong>. Mates themselves remain available in Clay Home, together with their conversations, memory, debates, and project collaboration.</p>' +
80
80
  '<p>Clay only gets sharper from here.</p>',
81
81
  },
82
82
  ];
@@ -0,0 +1,420 @@
1
+ var fs = require("fs");
2
+ var path = require("path");
3
+ var crypto = require("crypto");
4
+
5
+ var ACTIVE = { proposed: true, starting: true, running: true };
6
+ var MAX_RUNNING = 3;
7
+
8
+ function clean(value, max) {
9
+ if (typeof value !== "string") return "";
10
+ return value.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim().slice(0, max);
11
+ }
12
+
13
+ function userStorageKey(userId) {
14
+ return crypto.createHash("sha256").update(String(userId || "_single_user")).digest("hex").slice(0, 24) + ".json";
15
+ }
16
+
17
+ function attachWorkspaceAssignmentService(ctx) {
18
+ var storageDir = ctx.storageDir || null;
19
+ var records = [];
20
+ var unsubscribers = {};
21
+ var pendingRestartReplay = {};
22
+
23
+ function recordFile(userId) {
24
+ return storageDir ? path.join(storageDir, userStorageKey(userId)) : null;
25
+ }
26
+
27
+ function saveUser(userId) {
28
+ var file = recordFile(userId);
29
+ if (!file) return;
30
+ fs.mkdirSync(path.dirname(file), { recursive: true });
31
+ var owned = records.filter(function (record) { return record.userId === (userId || null); });
32
+ var temp = file + ".tmp." + process.pid;
33
+ fs.writeFileSync(temp, JSON.stringify(owned, null, 2), { mode: 0o600 });
34
+ fs.renameSync(temp, file);
35
+ }
36
+
37
+ function load() {
38
+ if (!storageDir) return;
39
+ var files = [];
40
+ try { files = fs.readdirSync(storageDir); } catch (e) { return; }
41
+ for (var i = 0; i < files.length; i++) {
42
+ if (!files[i].endsWith(".json") || files[i].indexOf(".tmp.") !== -1) continue;
43
+ try {
44
+ var loaded = JSON.parse(fs.readFileSync(path.join(storageDir, files[i]), "utf8"));
45
+ if (Array.isArray(loaded)) records = records.concat(loaded);
46
+ } catch (e) {}
47
+ }
48
+ var changedUsers = {};
49
+ for (var j = 0; j < records.length; j++) {
50
+ if (records[j].status !== "starting" && records[j].status !== "running") continue;
51
+ records[j].status = "interrupted";
52
+ records[j].error = "Clay restarted before this assignment completed.";
53
+ records[j].updatedAt = Date.now();
54
+ pendingRestartReplay[records[j].assignmentId] = true;
55
+ changedUsers[String(records[j].userId || "_single_user")] = records[j].userId || null;
56
+ }
57
+ var keys = Object.keys(changedUsers);
58
+ for (var k = 0; k < keys.length; k++) saveUser(changedUsers[keys[k]]);
59
+ }
60
+
61
+ function sourceKey(principal) {
62
+ return String(principal.userId || "_single_user") + ":" + principal.sourceProjectSlug + ":" + principal.sourceSessionRef;
63
+ }
64
+
65
+ function normalizePrincipal(principal) {
66
+ if (!principal || !principal.sourceProjectSlug) return null;
67
+ var project = ctx.getProjects().get(principal.sourceProjectSlug);
68
+ var manager = project && project.getSessionManager ? project.getSessionManager() : null;
69
+ var session = manager && manager.sessions ? manager.sessions.get(principal.sourceSessionId) : null;
70
+ if (!session && manager && manager.sessions && principal.sourceSessionRef) {
71
+ manager.sessions.forEach(function (candidate) {
72
+ if (!session && ctx.sessionRef(principal.sourceProjectSlug, candidate) === principal.sourceSessionRef) session = candidate;
73
+ });
74
+ }
75
+ if (!session) return null;
76
+ return Object.assign({}, principal, {
77
+ sourceSessionId: session.localId,
78
+ sourceSessionRef: ctx.sessionRef(principal.sourceProjectSlug, session),
79
+ });
80
+ }
81
+
82
+ function publicRecord(record) {
83
+ return {
84
+ assignmentId: record.assignmentId,
85
+ delivery: record.delivery || "new_session",
86
+ projectSlug: record.targetProjectSlug,
87
+ projectTitle: record.targetProjectTitle,
88
+ title: record.title,
89
+ task: record.task,
90
+ status: record.status,
91
+ sourceProjectSlug: record.sourceProjectSlug,
92
+ sourceSessionRef: record.sourceSessionRef,
93
+ targetSessionRef: record.targetSessionRef || null,
94
+ targetSessionTitle: record.targetSessionTitle || null,
95
+ resultSummary: record.resultSummary || "",
96
+ error: record.error || "",
97
+ createdAt: record.createdAt,
98
+ updatedAt: record.updatedAt,
99
+ };
100
+ }
101
+
102
+ function ownsProject(record, status) {
103
+ if (!status || status.isWorktree || status.isMate) return false;
104
+ if (ctx.isMultiUser()) return !!record.userId && status.projectOwnerId === record.userId;
105
+ return !status.projectOwnerId || (!!record.singleUserOwnerId && status.projectOwnerId === record.singleUserOwnerId);
106
+ }
107
+
108
+ function exactTarget(record) {
109
+ var project = ctx.getProjects().get(record.targetProjectSlug);
110
+ if (!project || !ownsProject(record, project.getStatus())) return null;
111
+ return project;
112
+ }
113
+
114
+ function exactSource(record) {
115
+ var project = ctx.getProjects().get(record.sourceProjectSlug);
116
+ if (!project) return null;
117
+ var status = project.getStatus();
118
+ if (!status || status.isMate !== true || status.mateId !== record.sourceMateId) return null;
119
+ if (ctx.isMultiUser() && status.projectOwnerId !== record.userId) return null;
120
+ var manager = project.getSessionManager();
121
+ var session = null;
122
+ if (manager && manager.sessions) {
123
+ manager.sessions.forEach(function (candidate) {
124
+ if (!session && ctx.sessionRef(record.sourceProjectSlug, candidate) === record.sourceSessionRef) session = candidate;
125
+ });
126
+ }
127
+ if (!session) return null;
128
+ if (ctx.isMultiUser() && session.ownerId !== record.userId) return null;
129
+ if (!ctx.isMultiUser() && session.ownerId && session.ownerId !== record.singleUserOwnerId) return null;
130
+ return { project: project, manager: manager, session: session };
131
+ }
132
+
133
+ function emit(record, eventType) {
134
+ var source = exactSource(record);
135
+ if (!source) return;
136
+ source.manager.sendAndRecord(source.session, {
137
+ type: eventType || "project_assignment_status",
138
+ assignment: publicRecord(record),
139
+ requestId: record.sourceRequestId || null,
140
+ });
141
+ }
142
+
143
+ function reconcileProject(projectSlug) {
144
+ for (var i = 0; i < records.length; i++) {
145
+ if (!pendingRestartReplay[records[i].assignmentId] || records[i].sourceProjectSlug !== projectSlug) continue;
146
+ if (!exactSource(records[i])) continue;
147
+ emit(records[i]);
148
+ delete pendingRestartReplay[records[i].assignmentId];
149
+ }
150
+ }
151
+
152
+ function propose(principal, args) {
153
+ principal = normalizePrincipal(principal);
154
+ if (!principal || !principal.sourceSessionRef) throw new Error("Project assignments require an exact session-bound Mate query.");
155
+ var projectSlug = clean(args.projectSlug, 120);
156
+ var title = clean(args.title, 160);
157
+ var task = clean(args.task, 12000);
158
+ if (!projectSlug || !title || !task) throw new Error("projectSlug, title, and task are required.");
159
+ var candidate = {
160
+ userId: principal.userId || null,
161
+ singleUserOwnerId: principal.singleUserOwnerId || null,
162
+ targetProjectSlug: projectSlug,
163
+ };
164
+ var target = exactTarget(candidate);
165
+ if (!target) throw new Error("Target project not found in the owned workspace.");
166
+ var key = sourceKey(principal);
167
+ var idempotency = crypto.createHash("sha256").update(key + "\u0000" + projectSlug + "\u0000" + title + "\u0000" + task).digest("hex");
168
+ for (var i = 0; i < records.length; i++) {
169
+ if (records[i].idempotencyKey === idempotency) return publicRecord(records[i]);
170
+ if (records[i].sourceKey === key && ACTIVE[records[i].status]) throw new Error("This conversation already has an active assignment.");
171
+ }
172
+ var now = Date.now();
173
+ var targetStatus = target.getStatus();
174
+ var record = {
175
+ assignmentId: "assignment_" + crypto.randomUUID(),
176
+ idempotencyKey: idempotency,
177
+ sourceKey: key,
178
+ userId: principal.userId || null,
179
+ singleUserOwnerId: principal.singleUserOwnerId || null,
180
+ sourceMateId: principal.mateId,
181
+ sourceProjectSlug: principal.sourceProjectSlug,
182
+ sourceSessionRef: principal.sourceSessionRef,
183
+ sourceRequestId: principal.sourceRequestId || null,
184
+ targetProjectSlug: projectSlug,
185
+ targetProjectTitle: clean(targetStatus.title || targetStatus.project || projectSlug, 160),
186
+ title: title,
187
+ task: task,
188
+ delivery: "new_session",
189
+ status: "proposed",
190
+ createdAt: now,
191
+ updatedAt: now,
192
+ };
193
+ if (!exactSource(record)) throw new Error("Source conversation is no longer available.");
194
+ records.push(record);
195
+ saveUser(record.userId);
196
+ emit(record, "project_assignment_proposal");
197
+ return publicRecord(record);
198
+ }
199
+
200
+ function proposeFollowUp(principal, args) {
201
+ principal = normalizePrincipal(principal);
202
+ if (!principal || !principal.sourceSessionRef) throw new Error("Project follow-ups require an exact session-bound Mate query.");
203
+ var projectSlug = clean(args.projectSlug, 120);
204
+ var targetSessionRef = clean(args.targetSessionRef, 100);
205
+ var title = clean(args.title, 160);
206
+ var task = clean(args.task, 12000);
207
+ if (!projectSlug || !targetSessionRef || !title || !task) throw new Error("projectSlug, targetSessionRef, title, and task are required.");
208
+ var key = sourceKey(principal);
209
+ var idempotency = crypto.createHash("sha256").update(key + "\u0000follow_up\u0000" + projectSlug + "\u0000" + targetSessionRef + "\u0000" + title + "\u0000" + task).digest("hex");
210
+ for (var i = 0; i < records.length; i++) {
211
+ if (records[i].idempotencyKey === idempotency) return publicRecord(records[i]);
212
+ if (records[i].sourceKey === key && ACTIVE[records[i].status]) throw new Error("This conversation already has an active assignment.");
213
+ }
214
+ var candidate = { userId: principal.userId || null, singleUserOwnerId: principal.singleUserOwnerId || null, targetProjectSlug: projectSlug };
215
+ var target = exactTarget(candidate);
216
+ if (!target || typeof target.inspectDelegatedFollowUp !== "function") throw new Error("Target project is unavailable for follow-ups.");
217
+ var inspected = target.inspectDelegatedFollowUp({ userId: principal.userId }, targetSessionRef);
218
+ var now = Date.now();
219
+ var targetStatus = target.getStatus();
220
+ var record = {
221
+ assignmentId: "assignment_" + crypto.randomUUID(), idempotencyKey: idempotency, sourceKey: key,
222
+ userId: principal.userId || null, singleUserOwnerId: principal.singleUserOwnerId || null,
223
+ sourceMateId: principal.mateId, sourceProjectSlug: principal.sourceProjectSlug,
224
+ sourceSessionRef: principal.sourceSessionRef, sourceRequestId: principal.sourceRequestId || null,
225
+ targetProjectSlug: projectSlug, targetProjectTitle: clean(targetStatus.title || targetStatus.project || projectSlug, 160),
226
+ targetSessionRef: inspected.sessionRef, targetSessionTitle: clean(inspected.title || "Existing session", 160),
227
+ title: title, task: task, delivery: "follow_up", status: "proposed", createdAt: now, updatedAt: now,
228
+ };
229
+ if (!exactSource(record)) throw new Error("Source conversation is no longer available.");
230
+ records.push(record);
231
+ saveUser(record.userId);
232
+ emit(record, "project_assignment_proposal");
233
+ return publicRecord(record);
234
+ }
235
+
236
+ function findOwned(userId, assignmentId) {
237
+ for (var i = 0; i < records.length; i++) {
238
+ if (records[i].assignmentId === assignmentId && records[i].userId === (userId || null)) return records[i];
239
+ }
240
+ return null;
241
+ }
242
+
243
+ function responseUserId(ws) {
244
+ return ctx.isMultiUser() && ws && ws._clayUser ? ws._clayUser.id : null;
245
+ }
246
+
247
+ function summarize(session) {
248
+ var text = "";
249
+ for (var i = (session && session.history || []).length - 1; i >= 0; i--) {
250
+ if (session.history[i].type === "delta") text = session.history[i].text + text;
251
+ else if (text && (session.history[i].type === "user_message" || session.history[i].type === "delegated_work" || session.history[i].type === "delegated_follow_up")) break;
252
+ }
253
+ return clean(text, 1200);
254
+ }
255
+
256
+ function settle(record, status, session, error) {
257
+ if (!ACTIVE[record.status]) return publicRecord(record);
258
+ record.status = status;
259
+ record.resultSummary = status === "completed" ? summarize(session) : "";
260
+ record.error = clean(error || "", 800);
261
+ record.updatedAt = Date.now();
262
+ if (unsubscribers[record.assignmentId]) unsubscribers[record.assignmentId]();
263
+ delete unsubscribers[record.assignmentId];
264
+ saveUser(record.userId);
265
+ emit(record);
266
+ return publicRecord(record);
267
+ }
268
+
269
+ function watch(record, manager, session) {
270
+ var unsubscribe = manager.subscribeSession(session.localId, function (event) {
271
+ if (event.type === "session_id") {
272
+ record.targetSessionRef = ctx.sessionRef(record.targetProjectSlug, session);
273
+ record.updatedAt = Date.now();
274
+ saveUser(record.userId);
275
+ emit(record);
276
+ } else if (event.type === "error") {
277
+ settle(record, "failed", session, event.text || "Assignment failed.");
278
+ } else if (event.type === "done" || event.type === "result") {
279
+ settle(record, session._lastTurnInterrupted ? "interrupted" : "completed", session, "");
280
+ }
281
+ });
282
+ if (unsubscribe) unsubscribers[record.assignmentId] = unsubscribe;
283
+ }
284
+
285
+ async function approve(record) {
286
+ if (record.status !== "proposed") return publicRecord(record);
287
+ var running = records.filter(function (item) {
288
+ return item.userId === record.userId && (item.status === "starting" || item.status === "running");
289
+ }).length;
290
+ if (running >= MAX_RUNNING) throw new Error("Too many assignments are already running.");
291
+ if (!exactSource(record)) throw new Error("Source conversation is no longer available.");
292
+ var target = exactTarget(record);
293
+ var followUp = record.delivery === "follow_up";
294
+ if (!target || (followUp ? typeof target.dispatchDelegatedFollowUp !== "function" : typeof target.createDelegatedSession !== "function")) throw new Error("Target project is no longer available.");
295
+ record.decision = "approve";
296
+ record.status = "starting";
297
+ record.updatedAt = Date.now();
298
+ saveUser(record.userId);
299
+ emit(record);
300
+ try {
301
+ var metadata = {
302
+ assignmentId: record.assignmentId,
303
+ title: record.title,
304
+ sourceMateId: record.sourceMateId,
305
+ sourceProjectSlug: record.sourceProjectSlug,
306
+ sourceSessionRef: record.sourceSessionRef,
307
+ };
308
+ var session;
309
+ if (followUp) {
310
+ session = await target.dispatchDelegatedFollowUp({ userId: record.userId }, record.targetSessionRef, record.task, metadata, function (existing) { watch(record, target.getSessionManager(), existing); });
311
+ } else {
312
+ session = await target.createDelegatedSession({ userId: record.userId }, record.task, metadata, function (created) { watch(record, target.getSessionManager(), created); });
313
+ }
314
+ if (record.status !== "starting") return publicRecord(record);
315
+ record.targetSessionRef = ctx.sessionRef(record.targetProjectSlug, session);
316
+ record.targetSessionId = session.localId;
317
+ record.status = "running";
318
+ record.updatedAt = Date.now();
319
+ saveUser(record.userId);
320
+ emit(record);
321
+ } catch (e) {
322
+ settle(record, "failed", null, e.message || String(e));
323
+ }
324
+ return publicRecord(record);
325
+ }
326
+
327
+ function cancel(record) {
328
+ if (record.status !== "proposed") return publicRecord(record);
329
+ record.decision = "cancel";
330
+ record.status = "cancelled";
331
+ record.updatedAt = Date.now();
332
+ saveUser(record.userId);
333
+ emit(record);
334
+ return publicRecord(record);
335
+ }
336
+
337
+ function getStatus(principal, assignmentId) {
338
+ principal = normalizePrincipal(principal);
339
+ if (!principal) throw new Error("Assignment not found for this conversation.");
340
+ var record = findOwned(principal.userId, assignmentId);
341
+ if (!record || record.sourceKey !== sourceKey(principal)) throw new Error("Assignment not found for this conversation.");
342
+ return publicRecord(record);
343
+ }
344
+
345
+ function validateResponseSource(ws, msg, record, routedProjectSlug) {
346
+ var source = exactSource(record);
347
+ if (!source) throw new Error("Source conversation is no longer available.");
348
+ var userId = responseUserId(ws);
349
+ if (record.userId !== (userId || null)) throw new Error("Assignment access denied.");
350
+ if (msg.sourceProjectSlug !== record.sourceProjectSlug || msg.sourceSessionRef !== record.sourceSessionRef) {
351
+ throw new Error("Assignment response does not match its source conversation.");
352
+ }
353
+ if (msg.surface === "home") {
354
+ var tap = ws && ws._homeChatTap;
355
+ if (!tap || tap.mateSlug !== record.sourceProjectSlug || tap.sessionId !== source.session.localId) {
356
+ throw new Error("Open the exact Home conversation before responding.");
357
+ }
358
+ if (msg.mateId !== tap.mateId || msg.sessionId !== tap.sessionReference || msg.requestId !== tap.requestId) {
359
+ throw new Error("Assignment response is stale for this Home conversation.");
360
+ }
361
+ } else if (routedProjectSlug !== record.sourceProjectSlug || !ws || ws._clayActiveSession !== source.session.localId) {
362
+ throw new Error("Open the exact project conversation before responding.");
363
+ }
364
+ return source;
365
+ }
366
+
367
+ async function respond(ws, msg, routedProjectSlug) {
368
+ var userId = responseUserId(ws);
369
+ var record = findOwned(userId, clean(msg.assignmentId, 100));
370
+ if (!record) throw new Error("Assignment proposal is stale or unavailable.");
371
+ validateResponseSource(ws, msg, record, routedProjectSlug);
372
+ var action = msg.action === "approve" ? "approve" : (msg.action === "cancel" ? "cancel" : "");
373
+ if (!action) throw new Error("Choose approve or cancel.");
374
+ if (record.status !== "proposed") {
375
+ if (record.decision === action) return publicRecord(record);
376
+ throw new Error("Assignment proposal has already been resolved.");
377
+ }
378
+ return action === "approve" ? approve(record) : cancel(record);
379
+ }
380
+
381
+ function handleMessage(ws, msg, routedProjectSlug) {
382
+ if (!msg || msg.type !== "project_assignment_response") return false;
383
+ respond(ws, msg, routedProjectSlug).then(function (assignment) {
384
+ if (ws.readyState !== 1) return;
385
+ if (msg.surface === "home") {
386
+ ws.send(JSON.stringify({ type: "home_project_assignment_status", assignment: assignment, mateId: msg.mateId, sessionId: msg.sessionId, requestId: msg.requestId }));
387
+ } else {
388
+ ws.send(JSON.stringify({ type: "project_assignment_status", assignment: assignment }));
389
+ }
390
+ }).catch(function (error) {
391
+ if (ws.readyState !== 1) return;
392
+ if (msg.surface === "home") {
393
+ var userId = responseUserId(ws);
394
+ var record = findOwned(userId, msg.assignmentId);
395
+ var assignment = record ? publicRecord(record) : { assignmentId: msg.assignmentId || null, status: "proposed" };
396
+ assignment.error = error.message || String(error);
397
+ ws.send(JSON.stringify({ type: "home_project_assignment_status", assignment: assignment, mateId: msg.mateId, sessionId: msg.sessionId, requestId: msg.requestId }));
398
+ } else {
399
+ ws.send(JSON.stringify({ type: "project_assignment_error", assignmentId: msg.assignmentId || null, error: error.message || String(error) }));
400
+ }
401
+ });
402
+ return true;
403
+ }
404
+
405
+ load();
406
+ return {
407
+ propose: propose,
408
+ proposeFollowUp: proposeFollowUp,
409
+ getStatus: getStatus,
410
+ respond: respond,
411
+ handleMessage: handleMessage,
412
+ reconcileProject: reconcileProject,
413
+ records: records,
414
+ };
415
+ }
416
+
417
+ module.exports = {
418
+ attachWorkspaceAssignmentService: attachWorkspaceAssignmentService,
419
+ userStorageKey: userStorageKey,
420
+ };