clay-server 4.0.0-beta.8 → 4.0.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 (185) hide show
  1. package/lib/capsule-display-floor.js +51 -0
  2. package/lib/capsule-frame-server.js +255 -0
  3. package/lib/capsule-mate-turn.js +105 -0
  4. package/lib/capsule-pig-logic.js +300 -0
  5. package/lib/capsule-server-runtimes.js +39 -0
  6. package/lib/capsules/pig/display.js +198 -0
  7. package/lib/capsules/pig/manifest.json +9 -0
  8. package/lib/capsules/pig/ui.json +84 -0
  9. package/lib/config.js +25 -1
  10. package/lib/daemon-projects.js +13 -8
  11. package/lib/daemon.js +127 -14
  12. package/lib/git-cli.js +95 -1
  13. package/lib/knowledge-import.js +436 -0
  14. package/lib/knowledge-record-store.js +188 -0
  15. package/lib/knowledge-search.js +240 -0
  16. package/lib/mate-knowledge-mcp-server.js +151 -0
  17. package/lib/mate-knowledge-migration.js +283 -0
  18. package/lib/mate-knowledge-service.js +488 -0
  19. package/lib/mate-knowledge-sync.js +482 -0
  20. package/lib/migrate-single-user.js +2 -0
  21. package/lib/notes-lifecycle.js +149 -0
  22. package/lib/notes.js +60 -3
  23. package/lib/os-users.js +3 -1
  24. package/lib/project-capsule-catalog.js +9 -1
  25. package/lib/project-capsule-turn.js +96 -0
  26. package/lib/project-connection.js +15 -1
  27. package/lib/project-file-history.js +223 -0
  28. package/lib/project-file-watch.js +77 -49
  29. package/lib/project-filesystem.js +31 -220
  30. package/lib/project-http.js +15 -1
  31. package/lib/project-knowledge.js +22 -0
  32. package/lib/project-log-context.js +172 -0
  33. package/lib/project-log-feedback-delivery.js +118 -0
  34. package/lib/project-logs-comments.js +137 -0
  35. package/lib/project-logs-context-state.js +42 -0
  36. package/lib/project-logs-mcp-server.js +328 -0
  37. package/lib/project-logs-query.js +185 -0
  38. package/lib/project-logs-root.js +75 -0
  39. package/lib/project-logs-schema.js +259 -0
  40. package/lib/project-logs-service.js +463 -0
  41. package/lib/project-logs-snapshot.js +205 -0
  42. package/lib/project-logs-store.js +495 -0
  43. package/lib/project-logs-versioning.js +132 -0
  44. package/lib/project-logs.js +400 -0
  45. package/lib/project-mate-interaction.js +14 -0
  46. package/lib/project-mate-knowledge.js +110 -0
  47. package/lib/project-memory.js +22 -0
  48. package/lib/project-message-delivery.js +70 -0
  49. package/lib/project-pair-lifecycle.js +460 -0
  50. package/lib/project-request-access.js +66 -0
  51. package/lib/project-session-handoff.js +1 -1
  52. package/lib/project-session-notes.js +64 -13
  53. package/lib/project-session-pair.js +159 -146
  54. package/lib/project-session-spawn.js +1 -1
  55. package/lib/project-sessions.js +58 -13
  56. package/lib/project-user-message.js +53 -4
  57. package/lib/project-worker-permission.js +430 -0
  58. package/lib/project-worker-proposal.js +208 -62
  59. package/lib/project.js +185 -6
  60. package/lib/public/app.js +63 -35
  61. package/lib/public/css/admin.css +2 -2
  62. package/lib/public/css/capsule-ui.css +38 -0
  63. package/lib/public/css/command-palette.css +19 -9
  64. package/lib/public/css/git-placard.css +154 -0
  65. package/lib/public/css/home-hub.css +4 -0
  66. package/lib/public/css/home-session-actions.css +66 -0
  67. package/lib/public/css/home-sidebar.css +66 -0
  68. package/lib/public/css/input.css +84 -3
  69. package/lib/public/css/messages.css +40 -1
  70. package/lib/public/css/mobile-nav.css +80 -0
  71. package/lib/public/css/notifications-center.css +67 -0
  72. package/lib/public/css/pane.css +97 -10
  73. package/lib/public/css/project-logs.css +198 -0
  74. package/lib/public/css/scheduler.css +1 -1
  75. package/lib/public/css/session-actions.css +16 -101
  76. package/lib/public/css/sidebar.css +103 -0
  77. package/lib/public/css/sticky-notes.css +237 -229
  78. package/lib/public/css/worker-proposal.css +36 -3
  79. package/lib/public/index.html +43 -11
  80. package/lib/public/modules/admin.js +8 -8
  81. package/lib/public/modules/app-connection.js +124 -6
  82. package/lib/public/modules/app-home-hub.js +12 -8
  83. package/lib/public/modules/app-message-cards.js +2 -1
  84. package/lib/public/modules/app-messages.js +92 -11
  85. package/lib/public/modules/app-misc.js +2 -14
  86. package/lib/public/modules/app-notifications.js +11 -7
  87. package/lib/public/modules/app-panels.js +3 -2
  88. package/lib/public/modules/app-projects.js +56 -18
  89. package/lib/public/modules/app-rendering.js +1 -0
  90. package/lib/public/modules/capsule-preference.js +44 -0
  91. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  92. package/lib/public/modules/clay-log-links.js +70 -0
  93. package/lib/public/modules/command-palette.js +52 -15
  94. package/lib/public/modules/context-sources.js +0 -3
  95. package/lib/public/modules/git-agent-sessions.js +85 -0
  96. package/lib/public/modules/git-panel.js +111 -84
  97. package/lib/public/modules/git-placard.js +161 -0
  98. package/lib/public/modules/home-chat-empty-state.js +39 -0
  99. package/lib/public/modules/home-chat-identity.js +9 -0
  100. package/lib/public/modules/home-conversations-sheet.js +102 -48
  101. package/lib/public/modules/home-mate-chat.js +15 -35
  102. package/lib/public/modules/home-session-actions.js +6 -0
  103. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  104. package/lib/public/modules/home-sidebar.js +7 -0
  105. package/lib/public/modules/home-surface-boot.js +7 -3
  106. package/lib/public/modules/home-tool-frame.js +182 -0
  107. package/lib/public/modules/home-tools.js +138 -1
  108. package/lib/public/modules/input.js +38 -7
  109. package/lib/public/modules/markdown.js +2 -0
  110. package/lib/public/modules/mate-sidebar.js +0 -8
  111. package/lib/public/modules/message-delivery.js +97 -0
  112. package/lib/public/modules/paste-modal.js +84 -0
  113. package/lib/public/modules/project-activation.js +45 -0
  114. package/lib/public/modules/project-logs-render.js +405 -0
  115. package/lib/public/modules/project-logs.js +461 -0
  116. package/lib/public/modules/project-removal-target.js +26 -0
  117. package/lib/public/modules/scheduler.js +17 -3
  118. package/lib/public/modules/session-actions.js +29 -105
  119. package/lib/public/modules/session-hierarchy.js +54 -0
  120. package/lib/public/modules/sidebar-mobile.js +40 -15
  121. package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
  122. package/lib/public/modules/sidebar-sessions.js +77 -11
  123. package/lib/public/modules/sidebar.js +17 -5
  124. package/lib/public/modules/split-group-helpers.js +8 -0
  125. package/lib/public/modules/split-pair-ui.js +15 -4
  126. package/lib/public/modules/split-view.js +5 -2
  127. package/lib/public/modules/split-worker-runtime.js +24 -0
  128. package/lib/public/modules/sticky-note-markdown.js +5 -0
  129. package/lib/public/modules/sticky-notes-browser.js +326 -0
  130. package/lib/public/modules/sticky-notes-card.js +360 -0
  131. package/lib/public/modules/sticky-notes-editor.js +291 -0
  132. package/lib/public/modules/sticky-notes-shared.js +90 -0
  133. package/lib/public/modules/sticky-notes.js +170 -946
  134. package/lib/public/modules/thinking-lifecycle.js +175 -0
  135. package/lib/public/modules/thinking-summary.js +28 -0
  136. package/lib/public/modules/thinking-view.js +75 -0
  137. package/lib/public/modules/tool-palette-order.js +143 -0
  138. package/lib/public/modules/tool-palette-overlays.js +159 -0
  139. package/lib/public/modules/tool-palette.js +21 -188
  140. package/lib/public/modules/tools.js +31 -123
  141. package/lib/public/modules/update-snooze.js +169 -0
  142. package/lib/public/modules/user-settings.js +4 -0
  143. package/lib/public/modules/worker-pane-lock.js +176 -0
  144. package/lib/public/modules/worker-proposal-state.js +16 -0
  145. package/lib/public/modules/worker-proposal.js +49 -20
  146. package/lib/public/style.css +3 -1
  147. package/lib/sdk-bridge.js +135 -46
  148. package/lib/sdk-message-processor.js +36 -19
  149. package/lib/server-experimental-settings.js +49 -0
  150. package/lib/server-global-ws.js +11 -0
  151. package/lib/server-home-chat-events.js +31 -1
  152. package/lib/server-home-chat.js +27 -4
  153. package/lib/server-home-clay-entry.js +1 -1
  154. package/lib/server-home-clay-session-links.js +34 -5
  155. package/lib/server-settings.js +5 -0
  156. package/lib/server-tools.js +127 -9
  157. package/lib/server.js +137 -22
  158. package/lib/session-driver-eligibility.js +38 -0
  159. package/lib/session-driver-orchestration.js +31 -0
  160. package/lib/session-hygiene.js +3 -0
  161. package/lib/session-notes-mcp-server.js +25 -5
  162. package/lib/session-pair-factory.js +247 -0
  163. package/lib/session-pair-mcp-server.js +46 -8
  164. package/lib/session-pair-prompts.js +79 -0
  165. package/lib/session-pair-turn-control.js +153 -0
  166. package/lib/session-provenance.js +119 -0
  167. package/lib/session-spawn-mcp-server.js +1 -1
  168. package/lib/session-split-groups.js +4 -1
  169. package/lib/session-title-policy.js +60 -0
  170. package/lib/session-visibility.js +41 -0
  171. package/lib/sessions.js +57 -10
  172. package/lib/tools-registry.js +89 -10
  173. package/lib/update-snooze.js +437 -0
  174. package/lib/users-experimental-preferences.js +30 -0
  175. package/lib/users-permissions.js +6 -6
  176. package/lib/users.js +22 -16
  177. package/lib/workspace-query-access.js +102 -0
  178. package/lib/workspace-query-service.js +24 -17
  179. package/lib/worktree.js +25 -38
  180. package/lib/ws-schema.js +38 -7
  181. package/lib/yoke/adapters/claude-worker.js +16 -6
  182. package/lib/yoke/adapters/claude.js +12 -0
  183. package/lib/yoke/adapters/codex.js +24 -14
  184. package/lib/yoke/vendor-registry.js +1 -1
  185. package/package.json +2 -2
@@ -0,0 +1,119 @@
1
+ var crypto = require("crypto");
2
+
3
+ var PROVENANCE_VERSION = 1;
4
+ var MAX_ORIGIN_LENGTH = 128;
5
+
6
+ function safeOriginId(value) {
7
+ if (typeof value !== "string") return null;
8
+ var trimmed = value.trim();
9
+ if (!trimmed || trimmed.length > MAX_ORIGIN_LENGTH) return null;
10
+ return trimmed;
11
+ }
12
+
13
+ function ensureOrigin(session) {
14
+ if (!session) return null;
15
+ var existing = safeOriginId(session.sessionOriginId);
16
+ if (existing) {
17
+ session.sessionOriginId = existing;
18
+ return existing;
19
+ }
20
+ session.sessionOriginId = crypto.randomUUID();
21
+ return session.sessionOriginId;
22
+ }
23
+
24
+ function isWorker(session) {
25
+ return !!(session && session.sessionProvenance && session.sessionProvenance.kind === "worker");
26
+ }
27
+
28
+ function nextWorkerGeneration(parentOriginId, sessions) {
29
+ var next = 1;
30
+ sessions.forEach(function (session) {
31
+ var provenance = session && session.sessionProvenance;
32
+ if (!provenance || provenance.kind !== "worker" || provenance.parentSessionOriginId !== parentOriginId) return;
33
+ var generation = Number.isInteger(provenance.generation) && provenance.generation > 0 ? provenance.generation : 0;
34
+ if (generation >= next) next = generation + 1;
35
+ });
36
+ return next;
37
+ }
38
+
39
+ function markWorker(driver, worker, sessions) {
40
+ if (!driver || !worker || driver === worker) throw new Error("Split Worker provenance requires distinct sessions");
41
+ if ((driver.ownerId || null) !== (worker.ownerId || null)) throw new Error("Split Worker provenance requires the same owner");
42
+ if (isWorker(driver)) throw new Error("A Split Worker cannot become a Driver");
43
+ var parentOriginId = ensureOrigin(driver);
44
+ ensureOrigin(worker);
45
+ worker.sessionProvenance = {
46
+ version: PROVENANCE_VERSION,
47
+ kind: "worker",
48
+ parentSessionOriginId: parentOriginId,
49
+ generation: nextWorkerGeneration(parentOriginId, sessions),
50
+ createdVia: "split-worker",
51
+ createdAt: Date.now(),
52
+ };
53
+ return worker.sessionProvenance;
54
+ }
55
+
56
+ function restore(meta, session) {
57
+ var storedOriginId = safeOriginId(meta && meta.sessionOriginId);
58
+ ensureOrigin(session);
59
+ var stored = meta && meta.sessionProvenance;
60
+ if (!stored || stored.kind !== "worker") return storedOriginId !== session.sessionOriginId;
61
+ var parentOriginId = safeOriginId(stored.parentSessionOriginId);
62
+ // A corrupt Worker record remains a Worker. It is projected as orphaned
63
+ // instead of silently gaining top-level Driver authority.
64
+ session.sessionProvenance = {
65
+ version: PROVENANCE_VERSION,
66
+ kind: "worker",
67
+ parentSessionOriginId: parentOriginId,
68
+ generation: Number.isInteger(stored.generation) && stored.generation > 0 ? stored.generation : null,
69
+ createdVia: stored.createdVia === "split-worker" ? "split-worker" : null,
70
+ createdAt: typeof stored.createdAt === "number" && isFinite(stored.createdAt) ? stored.createdAt : null,
71
+ };
72
+ return storedOriginId !== session.sessionOriginId || JSON.stringify(stored) !== JSON.stringify(session.sessionProvenance);
73
+ }
74
+
75
+ function metadata(session) {
76
+ var result = { sessionOriginId: ensureOrigin(session) };
77
+ if (isWorker(session)) result.sessionProvenance = Object.assign({}, session.sessionProvenance);
78
+ return result;
79
+ }
80
+
81
+ function hierarchyFor(sessions) {
82
+ var originMap = {};
83
+ var duplicateOrigins = {};
84
+ for (var i = 0; i < sessions.length; i++) {
85
+ var originId = safeOriginId(sessions[i] && sessions[i].sessionOriginId);
86
+ if (!originId) continue;
87
+ if (originMap[originId]) duplicateOrigins[originId] = true;
88
+ else originMap[originId] = sessions[i];
89
+ }
90
+ var result = {};
91
+ for (var j = 0; j < sessions.length; j++) {
92
+ var session = sessions[j];
93
+ if (!isWorker(session)) {
94
+ result[session.localId] = { role: "driver", parentSessionId: null, parentAvailable: false, generation: null };
95
+ continue;
96
+ }
97
+ var provenance = session.sessionProvenance;
98
+ var parentOriginId = safeOriginId(provenance.parentSessionOriginId);
99
+ var parent = duplicateOrigins[parentOriginId] ? null : originMap[parentOriginId];
100
+ var sameOwner = parent && (parent.ownerId || null) === (session.ownerId || null);
101
+ var validParent = sameOwner && !isWorker(parent) ? parent : null;
102
+ result[session.localId] = {
103
+ role: "worker",
104
+ parentSessionId: validParent ? validParent.localId : null,
105
+ parentAvailable: !!validParent,
106
+ generation: Number.isInteger(provenance.generation) && provenance.generation > 0 ? provenance.generation : null,
107
+ };
108
+ }
109
+ return result;
110
+ }
111
+
112
+ module.exports = {
113
+ ensureOrigin: ensureOrigin,
114
+ hierarchyFor: hierarchyFor,
115
+ isWorker: isWorker,
116
+ markWorker: markWorker,
117
+ metadata: metadata,
118
+ restore: restore,
119
+ };
@@ -27,7 +27,7 @@ function getToolDefs(handlers) {
27
27
  return [
28
28
  {
29
29
  name: "spawn_sessions",
30
- description: "Create sibling work sessions in this project. Set forkFromCurrent to copy this session's full conversation context into every child. Children share the project's working directory, so prefer parallel analysis or other non-conflicting work over concurrent edits. Results can be polled with check_spawned_sessions; spawned children cannot spawn further sessions.",
30
+ description: "Create ordinary sibling work sessions for background parallel work. This does not create a visible Driver/Split Worker pair and must not be used when the user asks for a Worker, paired pane, or split collaborator; use send_to_partner for that. Set forkFromCurrent to copy this session's full conversation context into every child. Children share the project's working directory, so prefer parallel analysis or other non-conflicting work over concurrent edits. Results can be polled with check_spawned_sessions; spawned children cannot spawn further sessions.",
31
31
  inputSchema: buildShape({
32
32
  sessions: { type: "string", description: "JSON array of objects: [{\"title\":\"Task title\",\"prompt\":\"Task instructions\"}]" },
33
33
  vendor: { type: "string", description: "Optional vendor id. Defaults to the parent session vendor, then the project default." },
@@ -280,7 +280,10 @@ function attachSplitGroups(ctx) {
280
280
  });
281
281
 
282
282
  function sendState(ws) {
283
- if (!ws || ws._clayPane) return;
283
+ if (!ws) return;
284
+ // Pane clients need the same owner-scoped pair roles as the parent shell.
285
+ // The Worker composer lock resolves its role from this projection; hiding
286
+ // the projection from panes leaves a configured Worker looking editable.
284
287
  ctx.sendTo(ws, { type: "split_groups", groups: store.listFor(ws) });
285
288
  }
286
289
 
@@ -0,0 +1,60 @@
1
+ // Session title policy shared by query startup and completed-turn processing.
2
+ // Initial titles are immediate, provisional UI labels. Model-generated titles
3
+ // replace them after enough conversation exists to describe the actual topic.
4
+
5
+ var DEFAULT_GENERATION_TURN = 2;
6
+ var HOME_GENERATION_TURN = 1;
7
+ var MAX_PROVISIONAL_CHARS = 60;
8
+ var LEGACY_SEARCH_TITLE = "This conversation began in Clay Studio's global search.";
9
+
10
+ function cleanText(value) {
11
+ if (typeof value !== "string") return "";
12
+ return value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
13
+ }
14
+
15
+ function firstRecordedUserMessage(session) {
16
+ var history = session && Array.isArray(session.history) ? session.history : [];
17
+ for (var i = 0; i < history.length; i++) {
18
+ if (history[i] && history[i].type === "user_message") {
19
+ var text = cleanText(history[i].text);
20
+ if (text) return text;
21
+ }
22
+ }
23
+ return "";
24
+ }
25
+
26
+ function selectInitialTitle(session, transportText) {
27
+ var existing = cleanText(session && session.title);
28
+ if (existing) return { title: existing, provisional: false };
29
+
30
+ // Prefer the visible message recorded in history. Query transports may wrap
31
+ // it in identical provider instructions, as global Ask Clay does.
32
+ var source = firstRecordedUserMessage(session) || cleanText(transportText);
33
+ if (source.length < 10) return { title: null, provisional: false };
34
+ return {
35
+ title: source.length > MAX_PROVISIONAL_CHARS ? source.substring(0, MAX_PROVISIONAL_CHARS) : source,
36
+ provisional: true,
37
+ };
38
+ }
39
+
40
+ function generationTurn(isMate) {
41
+ return isMate === true ? HOME_GENERATION_TURN : DEFAULT_GENERATION_TURN;
42
+ }
43
+
44
+ function repairLegacySearchTitle(session) {
45
+ var title = cleanText(session && session.title);
46
+ if (!session || title.indexOf(LEGACY_SEARCH_TITLE) !== 0) return false;
47
+ var source = firstRecordedUserMessage(session);
48
+ if (!source) return false;
49
+ session.title = source.length > MAX_PROVISIONAL_CHARS ? source.substring(0, MAX_PROVISIONAL_CHARS) : source;
50
+ session.titleAutoGenerated = false;
51
+ return true;
52
+ }
53
+
54
+ module.exports = {
55
+ DEFAULT_GENERATION_TURN: DEFAULT_GENERATION_TURN,
56
+ HOME_GENERATION_TURN: HOME_GENERATION_TURN,
57
+ selectInitialTitle: selectInitialTitle,
58
+ generationTurn: generationTurn,
59
+ repairLegacySearchTitle: repairLegacySearchTitle,
60
+ };
@@ -0,0 +1,41 @@
1
+ function normalize(value) {
2
+ return value === "shared" ? "shared" : "private";
3
+ }
4
+
5
+ function restore(meta) {
6
+ // Older releases persisted the shared default without recording intent.
7
+ return meta.sessionVisibilityExplicit === true ? normalize(meta.sessionVisibility) : "private";
8
+ }
9
+
10
+ function handleChange(ctx, ws, msg) {
11
+ if (typeof msg.sessionId !== "number" || (msg.visibility !== "shared" && msg.visibility !== "private")) return;
12
+ var session = ctx.sm.sessions.get(msg.sessionId);
13
+ if (!session) return;
14
+ if (ctx.usersModule.isMultiUser()) {
15
+ var user = ws._clayUser;
16
+ var project = ctx.getProjectAccess ? ctx.getProjectAccess() : { visibility: "public" };
17
+ if (!user || !session.ownerId || session.ownerId !== user.id ||
18
+ !ctx.usersModule.canAccessSession(user.id, session, project)) return;
19
+ } else if (session.ownerId) {
20
+ return;
21
+ }
22
+ ctx.sm.setSessionVisibility(msg.sessionId, msg.visibility);
23
+ revokeViewers(ctx, session);
24
+ }
25
+
26
+ function revokeViewers(ctx, session) {
27
+ if (ctx.usersModule.isMultiUser() && session.sessionVisibility === "private") {
28
+ var project = ctx.getProjectAccess ? ctx.getProjectAccess() : null;
29
+ ctx.clients.forEach(function(client) {
30
+ if (client._clayActiveSession !== session.localId) return;
31
+ var userId = client._clayUser && client._clayUser.id;
32
+ if (userId && ctx.usersModule.canAccessSession(userId, session, project)) return;
33
+ // Clear the server-side selection before reconnecting so no further
34
+ // session events or input can cross the revoked access boundary.
35
+ client._clayActiveSession = null;
36
+ if (client.readyState === 1) client.close(1008, "Session access changed");
37
+ });
38
+ }
39
+ }
40
+
41
+ module.exports = { normalize: normalize, restore: restore, handleChange: handleChange, revokeViewers: revokeViewers };
package/lib/sessions.js CHANGED
@@ -6,6 +6,8 @@ var users = require("./users");
6
6
  var { CODEX_DEFAULTS } = require("./codex-defaults");
7
7
  var hygiene = require("./session-hygiene");
8
8
  var backgroundTaskTiming = require("./background-task-timing");
9
+ var sessionProvenance = require("./session-provenance");
10
+ var sessionVisibility = require("./session-visibility");
9
11
 
10
12
  function createSessionManager(opts) {
11
13
  var cwd = opts.cwd;
@@ -165,7 +167,8 @@ function createSessionManager(opts) {
165
167
  if (session.dangerouslySkipPermissions) metaObj.dangerouslySkipPermissions = true;
166
168
  if (session.permissionMode) metaObj.permissionMode = session.permissionMode;
167
169
  if (session.permissionModeBeforeFullAccess) metaObj.permissionModeBeforeFullAccess = session.permissionModeBeforeFullAccess;
168
- if (session.sessionVisibility) metaObj.sessionVisibility = session.sessionVisibility;
170
+ metaObj.sessionVisibility = sessionVisibility.normalize(session.sessionVisibility);
171
+ if (session.sessionVisibilityExplicit) metaObj.sessionVisibilityExplicit = true;
169
172
  if (session.bookmarked) metaObj.bookmarked = true;
170
173
  if (typeof session.favoriteOrder === "number") metaObj.favoriteOrder = session.favoriteOrder;
171
174
  if (session.lastRewindUuid) metaObj.lastRewindUuid = session.lastRewindUuid;
@@ -180,6 +183,9 @@ function createSessionManager(opts) {
180
183
  if (session.homeDebateInitialTopic) metaObj.homeDebateInitialTopic = session.homeDebateInitialTopic;
181
184
  if (session.homeClayEntryRequestId) metaObj.homeClayEntryRequestId = session.homeClayEntryRequestId;
182
185
  if (session.homeClayEntryMode) metaObj.homeClayEntryMode = session.homeClayEntryMode;
186
+ var provenanceMeta = sessionProvenance.metadata(session);
187
+ metaObj.sessionOriginId = provenanceMeta.sessionOriginId;
188
+ if (provenanceMeta.sessionProvenance) metaObj.sessionProvenance = provenanceMeta.sessionProvenance;
183
189
  var meta = JSON.stringify(metaObj);
184
190
  var lines = [meta];
185
191
  for (var i = 0; i < session.history.length; i++) {
@@ -300,6 +306,8 @@ function createSessionManager(opts) {
300
306
  if (m.homeClayEntryRequestId) session.homeClayEntryRequestId = m.homeClayEntryRequestId;
301
307
  if (m.homeClayEntryMode) session.homeClayEntryMode = m.homeClayEntryMode;
302
308
  if (m.ownerId) session.ownerId = m.ownerId;
309
+ session.sessionOriginId = m.sessionOriginId || null;
310
+ session._provenanceMigrationNeeded = sessionProvenance.restore(m, session);
303
311
  // Born-TUI session: PTY is gone after restart, but the cliSessionId
304
312
  // is still resumable via `claude --resume <id>`. We mark the mode
305
313
  // here so it shows up in the sidebar with the right icon; the
@@ -315,11 +323,26 @@ function createSessionManager(opts) {
315
323
  session.terminalId = null;
316
324
  session.runtimeMode = null;
317
325
  session.runtimeTerminalId = null;
318
- session.sessionVisibility = m.sessionVisibility || "shared";
326
+ session.sessionVisibility = sessionVisibility.restore(m);
327
+ session.sessionVisibilityExplicit = m.sessionVisibilityExplicit === true;
319
328
  session.bookmarked = !!m.bookmarked;
320
329
  session.favoriteOrder = typeof m.favoriteOrder === "number" ? m.favoriteOrder : null;
321
330
  sessions.set(localId, session);
322
331
  }
332
+
333
+ // Versionless legacy records are safe top-level Drivers. Assigning an
334
+ // opaque origin ID is the only migration: no title, slug or active split
335
+ // state is used to invent historical parentage.
336
+ sessions.forEach(function (session) {
337
+ if (!session.cliSessionId || !session._provenanceMigrationNeeded) return;
338
+ var activityTime = session.lastActivity;
339
+ saveSessionFile(session);
340
+ try {
341
+ var preservedTime = new Date(activityTime);
342
+ fs.utimesSync(sessionFilePath(session.cliSessionId), preservedTime, preservedTime);
343
+ } catch (e) {}
344
+ delete session._provenanceMigrationNeeded;
345
+ });
323
346
  }
324
347
 
325
348
  // Adopt orphaned CLI sessions from ~/.claude/projects/<encoded-cwd>/ as
@@ -427,7 +450,7 @@ function createSessionManager(opts) {
427
450
  terminalId: null,
428
451
  runtimeMode: null,
429
452
  runtimeTerminalId: null,
430
- sessionVisibility: "shared",
453
+ sessionVisibility: "private",
431
454
  bookmarked: false,
432
455
  favoriteOrder: null,
433
456
  };
@@ -516,7 +539,7 @@ function createSessionManager(opts) {
516
539
  loop: loop,
517
540
  spawn: s.spawn || null,
518
541
  ownerId: s.ownerId || null,
519
- sessionVisibility: s.sessionVisibility || "shared",
542
+ sessionVisibility: sessionVisibility.normalize(s.sessionVisibility),
520
543
  bookmarked: !!s.bookmarked,
521
544
  favoriteOrder: typeof s.favoriteOrder === "number" ? s.favoriteOrder : null,
522
545
  unread: unreadMap[s.localId] || 0,
@@ -532,6 +555,19 @@ function createSessionManager(opts) {
532
555
  };
533
556
  }
534
557
 
558
+ function mapSessionsForClient(source, clientActiveId, wsUnread) {
559
+ var hierarchy = sessionProvenance.hierarchyFor(source);
560
+ return source.map(function (session) {
561
+ var mapped = mapSessionForClient(session, clientActiveId, wsUnread);
562
+ var node = hierarchy[session.localId];
563
+ mapped.sessionRole = node.role;
564
+ mapped.parentSessionId = node.parentSessionId;
565
+ mapped.parentAvailable = node.parentAvailable;
566
+ mapped.workerGeneration = node.generation;
567
+ return mapped;
568
+ });
569
+ }
570
+
535
571
  function getVisibleSessions() {
536
572
  var multiUser = users.isMultiUser();
537
573
  return [...sessions.values()].filter(function (s) {
@@ -554,14 +590,14 @@ function createSessionManager(opts) {
554
590
  if (ws.readyState === 1) {
555
591
  ws.send(JSON.stringify({
556
592
  type: "session_list",
557
- sessions: filtered.map(function (s) { return mapSessionForClient(s, clientActiveId, wsUnread); }),
593
+ sessions: mapSessionsForClient(filtered, clientActiveId, wsUnread),
558
594
  }));
559
595
  }
560
596
  });
561
597
  } else {
562
598
  send({
563
599
  type: "session_list",
564
- sessions: allVisible.map(function (s) { return mapSessionForClient(s); }),
600
+ sessions: mapSessionsForClient(allVisible),
565
601
  });
566
602
  }
567
603
  }
@@ -587,7 +623,8 @@ function createSessionManager(opts) {
587
623
  history: [],
588
624
  messageUUIDs: [],
589
625
  ownerId: (sessionOpts && sessionOpts.ownerId) || null,
590
- sessionVisibility: (sessionOpts && sessionOpts.sessionVisibility) || "shared",
626
+ sessionVisibility: sessionVisibility.normalize(sessionOpts && sessionOpts.sessionVisibility),
627
+ sessionVisibilityExplicit: !!(sessionOpts && sessionOpts.sessionVisibility === "shared"),
591
628
  bookmarked: false,
592
629
  favoriteOrder: null,
593
630
  vendor: (sessionOpts && sessionOpts.vendor) || null,
@@ -598,7 +635,9 @@ function createSessionManager(opts) {
598
635
  permissionMode: (sessionOpts && sessionOpts.permissionMode) || null,
599
636
  permissionModeBeforeFullAccess: null,
600
637
  terminalId: null,
638
+ sessionOriginId: null,
601
639
  };
640
+ sessionProvenance.ensureOrigin(session);
602
641
  sessions.set(localId, session);
603
642
  switchSession(localId, targetWs);
604
643
  return session;
@@ -626,7 +665,8 @@ function createSessionManager(opts) {
626
665
  history: [],
627
666
  messageUUIDs: [],
628
667
  ownerId: (sessionOpts && sessionOpts.ownerId) || null,
629
- sessionVisibility: (sessionOpts && sessionOpts.sessionVisibility) || "shared",
668
+ sessionVisibility: sessionVisibility.normalize(sessionOpts && sessionOpts.sessionVisibility),
669
+ sessionVisibilityExplicit: !!(sessionOpts && sessionOpts.sessionVisibility === "shared"),
630
670
  bookmarked: false,
631
671
  favoriteOrder: null,
632
672
  vendor: (sessionOpts && sessionOpts.vendor) || null,
@@ -637,7 +677,9 @@ function createSessionManager(opts) {
637
677
  permissionMode: (sessionOpts && sessionOpts.permissionMode) || null,
638
678
  permissionModeBeforeFullAccess: null,
639
679
  terminalId: null,
680
+ sessionOriginId: null,
640
681
  };
682
+ sessionProvenance.ensureOrigin(session);
641
683
  sessions.set(localId, session);
642
684
  return session;
643
685
  }
@@ -879,7 +921,8 @@ function createSessionManager(opts) {
879
921
  // Send to active clients without recording to history/disk (ephemeral data)
880
922
  if (sendEach) {
881
923
  var data = JSON.stringify(obj);
882
- sendEach(function (ws) {
924
+ sendEach(function (ws, filterFn) {
925
+ if (filterFn && !filterFn(session)) return;
883
926
  if (session.homeDebatePlanning === true) return;
884
927
  if (ws._clayActiveSession === session.localId && ws.readyState === 1) {
885
928
  ws.send(data);
@@ -910,7 +953,8 @@ function createSessionManager(opts) {
910
953
  // Multi-user: send to clients whose active session matches this one
911
954
  var data = JSON.stringify(outgoingObj);
912
955
  var ioData = null;
913
- sendEach(function (ws) {
956
+ sendEach(function (ws, filterFn) {
957
+ if (filterFn && !filterFn(session)) return;
914
958
  if (session.homeDebatePlanning === true) return;
915
959
  if (ws._clayActiveSession === session.localId) {
916
960
  if (ws.readyState === 1) ws.send(data);
@@ -1248,11 +1292,14 @@ function createSessionManager(opts) {
1248
1292
  searchSessionContent: searchSessionContent,
1249
1293
  setResolveLoopInfo: setResolveLoopInfo,
1250
1294
  mapSessionForClient: mapSessionForClient,
1295
+ mapSessionsForClient: mapSessionsForClient,
1251
1296
  migrateSessionTitles: migrateSessionTitles,
1252
1297
  setSessionVisibility: function (localId, visibility) {
1253
1298
  var session = sessions.get(localId);
1254
1299
  if (!session) return { error: "Session not found" };
1300
+ if (visibility !== "shared" && visibility !== "private") return { error: "Invalid session visibility" };
1255
1301
  session.sessionVisibility = visibility;
1302
+ session.sessionVisibilityExplicit = true;
1256
1303
  saveSessionFile(session);
1257
1304
  broadcastSessionList();
1258
1305
  return { ok: true };
@@ -3,9 +3,10 @@ var path = require("path");
3
3
  var config = require("./config");
4
4
  var toolUiSpec = require("./tool-ui-spec");
5
5
  var toolCapsuleSource = require("./tool-capsule-source");
6
+ var capsuleFloor = require("./capsule-display-floor");
6
7
 
7
8
  var CAPSULES_ROOT = path.join(__dirname, "capsules");
8
- var SEED_MARKER = ".capsules-v7";
9
+ var SEED_MARKER = ".capsules-v10";
9
10
  var REMOVED_BUILTIN_IDS = ["board", "translator", "scratchpad"];
10
11
  var UI_NODE_TYPES = toolUiSpec.UI_NODE_TYPES;
11
12
  var DISCOVERY_FIELD_MAX_LENGTH = 240;
@@ -98,6 +99,11 @@ function validateManifest(manifest, opts) {
98
99
  if (manifest.skills !== undefined && typeof manifest.skills !== "string") {
99
100
  throw new Error("Tool skills must be markdown text.");
100
101
  }
102
+ // The floor gate always validates the Display itself, so a manifest may
103
+ // never carry a claim about it.
104
+ if (manifest.displayFloor !== undefined) {
105
+ throw new Error("Tool displayFloor cannot be authored; Clay validates the Display itself.");
106
+ }
101
107
  validateDiscoveryField(manifest, "description", "description");
102
108
  validateDiscoveryField(manifest, "useWhen", "useWhen");
103
109
  if (manifest.permissions !== undefined) {
@@ -129,33 +135,90 @@ function toolDirectory(ctx, id) {
129
135
  return path.join(resolveToolsRoot(ctx), id);
130
136
  }
131
137
 
138
+ function hasAnySeedMarker(root) {
139
+ try {
140
+ return fs.readdirSync(root).some(function (name) { return /^\.capsules-v\d+$/.test(name); });
141
+ } catch (e) {
142
+ return false;
143
+ }
144
+ }
145
+
132
146
  function seedBuiltInCapsules(ctx) {
133
147
  var root = resolveToolsRoot(ctx);
134
148
  var marker = path.join(root, SEED_MARKER);
135
149
  if (fs.existsSync(marker)) return;
150
+ // The removed-built-in cleanup is a one-time migration. Any earlier marker
151
+ // means it already ran, so a later marker bump must not delete a Capsule the
152
+ // user has since authored under one of those IDs.
153
+ var migrated = hasAnySeedMarker(root);
136
154
  fs.mkdirSync(root, { recursive: true });
137
- for (var ri = 0; ri < REMOVED_BUILTIN_IDS.length; ri++) {
155
+ for (var ri = 0; !migrated && ri < REMOVED_BUILTIN_IDS.length; ri++) {
138
156
  fs.rmSync(path.join(root, REMOVED_BUILTIN_IDS[ri]), { recursive: true, force: true });
139
157
  }
140
- // Clay ships no built-in Capsules any more, so lib/capsules/ is absent from a
141
- // fresh checkout and from the npm tarball (neither git nor npm carries an
142
- // empty directory). Seeding still owns the removed-built-in migration above
143
- // and the marker below, so an absent root means "nothing to copy" rather than
144
- // a failure. Anything other than a missing root is still a real error.
158
+ // lib/capsules/ holds the shipped Capsules, currently the Pig validation
159
+ // Capsule. It may still be absent from an older checkout or tarball, and
160
+ // seeding also owns the removed-built-in migration above and the marker
161
+ // below, so a missing root means "nothing to copy" rather than a failure.
162
+ // Anything other than a missing root is still a real error.
145
163
  var entries = [];
146
164
  try {
147
165
  entries = fs.readdirSync(CAPSULES_ROOT, { withFileTypes: true });
148
166
  } catch (e) {
149
167
  if (e.code !== "ENOENT") throw e;
150
168
  }
169
+ // A previously seeded server-runtime Capsule that is no longer shipped is
170
+ // always a stale copy: the server runtime is reserved for lib/capsules/, so
171
+ // no user-authored Capsule can ever match this test. Remove it, or it would
172
+ // linger as a dead entry with no Logic behind it.
173
+ var shippedIds = Object.create(null);
174
+ for (var si = 0; si < entries.length; si++) {
175
+ if (entries[si].isDirectory()) shippedIds[entries[si].name] = true;
176
+ }
177
+ var rootEntries = [];
178
+ try {
179
+ rootEntries = fs.readdirSync(root, { withFileTypes: true });
180
+ } catch (e) {
181
+ if (e.code !== "ENOENT") throw e;
182
+ }
183
+ for (var re = 0; re < rootEntries.length; re++) {
184
+ if (!rootEntries[re].isDirectory() || shippedIds[rootEntries[re].name]) continue;
185
+ try {
186
+ var staleManifest = JSON.parse(fs.readFileSync(path.join(root, rootEntries[re].name, "manifest.json"), "utf8"));
187
+ if (staleManifest && staleManifest.runtime === "server") {
188
+ fs.rmSync(path.join(root, rootEntries[re].name), { recursive: true, force: true });
189
+ }
190
+ } catch (e) { /* not a capsule directory; leave it alone */ }
191
+ }
192
+
151
193
  var seeded = [];
194
+ var SOURCE_FILES = ["manifest.json", "ui.json", "logic.js", "display.js", "skills.md"];
152
195
  for (var i = 0; i < entries.length; i++) {
153
196
  if (!entries[i].isDirectory()) continue;
154
197
  var destination = path.join(root, entries[i].name);
155
198
  if (!fs.existsSync(destination)) {
156
199
  fs.cpSync(path.join(CAPSULES_ROOT, entries[i].name), destination, { recursive: true });
157
200
  seeded.push(entries[i].name);
201
+ continue;
202
+ }
203
+ var shippedManifestPath = path.join(CAPSULES_ROOT, entries[i].name, "manifest.json");
204
+ var shippedRuntime = "worker";
205
+ try {
206
+ shippedRuntime = JSON.parse(fs.readFileSync(shippedManifestPath, "utf8")).runtime || "worker";
207
+ } catch (e) { /* fall through to the conservative worker path */ }
208
+ if (shippedRuntime === "server") {
209
+ // Server-runtime Capsules are Clay-owned: their source is not editable
210
+ // by users or Mates, so a marker bump syncs the shipped source files
211
+ // exactly, dropping ones the shipped set no longer carries. The
212
+ // Capsule's datastore and anything else in the directory is preserved.
213
+ for (var sf = 0; sf < SOURCE_FILES.length; sf++) {
214
+ var shippedFile = path.join(CAPSULES_ROOT, entries[i].name, SOURCE_FILES[sf]);
215
+ var localFile = path.join(destination, SOURCE_FILES[sf]);
216
+ if (fs.existsSync(shippedFile)) fs.copyFileSync(shippedFile, localFile);
217
+ else fs.rmSync(localFile, { force: true });
218
+ }
158
219
  } else {
220
+ // Worker Capsules may have been edited by the user, so only files the
221
+ // local copy is missing entirely are filled in.
159
222
  var sourceFiles = fs.readdirSync(path.join(CAPSULES_ROOT, entries[i].name));
160
223
  for (var fi = 0; fi < sourceFiles.length; fi++) {
161
224
  var sourceFile = path.join(CAPSULES_ROOT, entries[i].name, sourceFiles[fi]);
@@ -185,6 +248,11 @@ function getTool(ctx, id) {
185
248
  manifest: Object.assign({}, manifest, { runtime: runtime }),
186
249
  logicSource: runtime === "worker" ? fs.readFileSync(logicPath, "utf8") : null,
187
250
  uiTree: uiTree,
251
+ // Rich Display is opted into by shipping a display.js next to the floor,
252
+ // the same way logic.js opts into the worker runtime. Only the flag
253
+ // travels; the source is served solely by the isolated frame origin, so
254
+ // it can never leak into a manifest, a prompt, or a Mate surface.
255
+ hasRichDisplay: fs.existsSync(path.join(directory, "display.js")),
188
256
  metadata: toolCapsuleSource.sanitizedMetadata(resolveToolsRoot(ctx), id),
189
257
  };
190
258
  }
@@ -199,8 +267,14 @@ function listTools(ctx) {
199
267
  if (entries[i].name.charAt(0) === ".") continue;
200
268
  try {
201
269
  var tool = getTool(ctx, entries[i].name);
202
- if (tool) manifests.push(tool.manifest);
203
- else manifests.push({ id: entries[i].name, error: "Capsule requires manifest.json and ui.json." });
270
+ if (tool) {
271
+ // The flat manifest carries its own Display alongside it, so a
272
+ // Mate-facing gate can validate the real tree without a second read
273
+ // and without trusting a claim. Non-enumerable, so the tree never
274
+ // leaks into a projection, a prompt, or a serialized message.
275
+ Object.defineProperty(tool.manifest, "uiTree", { value: tool.uiTree, enumerable: false, configurable: true });
276
+ manifests.push(tool.manifest);
277
+ } else manifests.push({ id: entries[i].name, error: "Capsule requires manifest.json and ui.json." });
204
278
  } catch (e) {
205
279
  manifests.push({ id: entries[i].name, error: e.message });
206
280
  }
@@ -216,6 +290,7 @@ function installTool(ctx, input) {
216
290
  throw new Error("Tool logic source is required.");
217
291
  }
218
292
  validateUiTreeForManifest(input.uiTree, input.manifest);
293
+ capsuleFloor.assertFloor(input.uiTree, input.manifest);
219
294
  var manifest = Object.assign({}, input.manifest, { name: input.manifest.name.trim(), runtime: "worker" });
220
295
  var directory = toolDirectory(ctx, manifest.id);
221
296
  if (fs.existsSync(directory)) throw new Error("A Capsule with this ID already exists. Read its source and use the update tool instead.");
@@ -253,7 +328,10 @@ function updateTool(ctx, id, input) {
253
328
  if (!tool) throw new Error("Tool not found.");
254
329
  toolCapsuleSource.updateSource(toolDirectory(ctx, id), input, {
255
330
  validateManifest: validateManifest,
256
- validateUiNode: function (uiTree) { return validateUiTreeForManifest(uiTree, input.manifest); },
331
+ validateUiNode: function (uiTree) {
332
+ validateUiTreeForManifest(uiTree, input.manifest);
333
+ return capsuleFloor.assertFloor(uiTree, input.manifest);
334
+ },
257
335
  });
258
336
  return getTool(ctx, id);
259
337
  }
@@ -268,6 +346,7 @@ function removeTool(ctx, id) {
268
346
 
269
347
  module.exports = {
270
348
  CAPSULES_ROOT: CAPSULES_ROOT,
349
+ SEED_MARKER: SEED_MARKER,
271
350
  UI_NODE_TYPES: UI_NODE_TYPES,
272
351
  resolveToolsRoot: resolveToolsRoot,
273
352
  validateToolId: validateToolId,