clay-server 4.0.0-beta.2 → 4.0.0-beta.21

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 (177) hide show
  1. package/lib/background-task-timing.js +105 -0
  2. package/lib/capsule-display-floor.js +51 -0
  3. package/lib/capsule-frame-server.js +255 -0
  4. package/lib/capsule-mate-turn.js +105 -0
  5. package/lib/capsule-pig-logic.js +300 -0
  6. package/lib/capsule-server-runtimes.js +39 -0
  7. package/lib/capsules/pig/display.js +198 -0
  8. package/lib/capsules/pig/manifest.json +9 -0
  9. package/lib/capsules/pig/ui.json +84 -0
  10. package/lib/config.js +25 -1
  11. package/lib/daemon-projects.js +13 -8
  12. package/lib/daemon.js +127 -14
  13. package/lib/git-cli.js +95 -1
  14. package/lib/knowledge-import.js +436 -0
  15. package/lib/knowledge-record-store.js +188 -0
  16. package/lib/knowledge-search.js +240 -0
  17. package/lib/mate-knowledge-mcp-server.js +151 -0
  18. package/lib/mate-knowledge-migration.js +283 -0
  19. package/lib/mate-knowledge-service.js +488 -0
  20. package/lib/mate-knowledge-sync.js +482 -0
  21. package/lib/migrate-single-user.js +2 -0
  22. package/lib/notes-lifecycle.js +149 -0
  23. package/lib/notes.js +60 -3
  24. package/lib/os-users.js +3 -1
  25. package/lib/project-capsule-catalog.js +9 -1
  26. package/lib/project-capsule-turn.js +96 -0
  27. package/lib/project-connection.js +14 -1
  28. package/lib/project-http.js +15 -1
  29. package/lib/project-knowledge.js +22 -0
  30. package/lib/project-log-context.js +172 -0
  31. package/lib/project-log-feedback-delivery.js +118 -0
  32. package/lib/project-logs-comments.js +137 -0
  33. package/lib/project-logs-context-state.js +42 -0
  34. package/lib/project-logs-mcp-server.js +328 -0
  35. package/lib/project-logs-query.js +185 -0
  36. package/lib/project-logs-root.js +75 -0
  37. package/lib/project-logs-schema.js +259 -0
  38. package/lib/project-logs-service.js +463 -0
  39. package/lib/project-logs-snapshot.js +205 -0
  40. package/lib/project-logs-store.js +495 -0
  41. package/lib/project-logs-versioning.js +132 -0
  42. package/lib/project-logs.js +400 -0
  43. package/lib/project-mate-interaction.js +14 -0
  44. package/lib/project-mate-knowledge.js +110 -0
  45. package/lib/project-memory.js +22 -0
  46. package/lib/project-message-delivery.js +70 -0
  47. package/lib/project-pair-lifecycle.js +460 -0
  48. package/lib/project-session-handoff.js +1 -1
  49. package/lib/project-session-notes.js +64 -13
  50. package/lib/project-session-pair.js +159 -146
  51. package/lib/project-session-spawn.js +1 -1
  52. package/lib/project-sessions.js +58 -13
  53. package/lib/project-user-message.js +53 -4
  54. package/lib/project-vendor-login.js +20 -3
  55. package/lib/project-worker-permission.js +430 -0
  56. package/lib/project-worker-proposal.js +208 -62
  57. package/lib/project.js +178 -6
  58. package/lib/public/app.js +58 -35
  59. package/lib/public/css/admin.css +2 -2
  60. package/lib/public/css/capsule-ui.css +38 -0
  61. package/lib/public/css/command-palette.css +19 -9
  62. package/lib/public/css/git-placard.css +154 -0
  63. package/lib/public/css/home-hub.css +4 -0
  64. package/lib/public/css/home-session-actions.css +66 -0
  65. package/lib/public/css/home-sidebar.css +66 -0
  66. package/lib/public/css/input.css +163 -28
  67. package/lib/public/css/mobile-nav.css +80 -0
  68. package/lib/public/css/notifications-center.css +67 -0
  69. package/lib/public/css/pane.css +52 -0
  70. package/lib/public/css/project-logs.css +198 -0
  71. package/lib/public/css/scheduler.css +1 -1
  72. package/lib/public/css/session-actions.css +16 -101
  73. package/lib/public/css/sidebar.css +103 -0
  74. package/lib/public/css/sticky-notes.css +237 -229
  75. package/lib/public/css/worker-proposal.css +34 -1
  76. package/lib/public/index.html +45 -11
  77. package/lib/public/modules/admin.js +8 -8
  78. package/lib/public/modules/app-connection.js +109 -4
  79. package/lib/public/modules/app-home-hub.js +19 -7
  80. package/lib/public/modules/app-message-cards.js +2 -1
  81. package/lib/public/modules/app-messages.js +84 -7
  82. package/lib/public/modules/app-misc.js +2 -14
  83. package/lib/public/modules/app-notifications.js +11 -7
  84. package/lib/public/modules/app-panels.js +46 -4
  85. package/lib/public/modules/app-projects.js +13 -8
  86. package/lib/public/modules/app-rendering.js +1 -0
  87. package/lib/public/modules/background-tasks-ui.js +139 -16
  88. package/lib/public/modules/capsule-preference.js +44 -0
  89. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  90. package/lib/public/modules/clay-log-links.js +70 -0
  91. package/lib/public/modules/command-palette.js +52 -15
  92. package/lib/public/modules/context-sources.js +0 -3
  93. package/lib/public/modules/git-agent-sessions.js +85 -0
  94. package/lib/public/modules/git-panel.js +111 -84
  95. package/lib/public/modules/git-placard.js +161 -0
  96. package/lib/public/modules/home-chat-empty-state.js +39 -0
  97. package/lib/public/modules/home-chat-identity.js +9 -0
  98. package/lib/public/modules/home-conversations-sheet.js +102 -48
  99. package/lib/public/modules/home-mate-chat.js +15 -35
  100. package/lib/public/modules/home-session-actions.js +6 -0
  101. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  102. package/lib/public/modules/home-sidebar.js +7 -0
  103. package/lib/public/modules/home-tool-frame.js +182 -0
  104. package/lib/public/modules/home-tools.js +138 -1
  105. package/lib/public/modules/input.js +38 -7
  106. package/lib/public/modules/markdown.js +2 -0
  107. package/lib/public/modules/mate-sidebar.js +0 -8
  108. package/lib/public/modules/message-delivery.js +97 -0
  109. package/lib/public/modules/paste-modal.js +84 -0
  110. package/lib/public/modules/project-logs-render.js +405 -0
  111. package/lib/public/modules/project-logs.js +461 -0
  112. package/lib/public/modules/project-removal-target.js +26 -0
  113. package/lib/public/modules/scheduler.js +17 -3
  114. package/lib/public/modules/session-actions.js +29 -105
  115. package/lib/public/modules/session-hierarchy.js +54 -0
  116. package/lib/public/modules/sidebar-mobile.js +40 -15
  117. package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
  118. package/lib/public/modules/sidebar-sessions.js +77 -11
  119. package/lib/public/modules/sidebar.js +17 -5
  120. package/lib/public/modules/split-group-helpers.js +8 -0
  121. package/lib/public/modules/split-view.js +5 -2
  122. package/lib/public/modules/sticky-note-markdown.js +5 -0
  123. package/lib/public/modules/sticky-notes-browser.js +326 -0
  124. package/lib/public/modules/sticky-notes-card.js +360 -0
  125. package/lib/public/modules/sticky-notes-editor.js +291 -0
  126. package/lib/public/modules/sticky-notes-shared.js +90 -0
  127. package/lib/public/modules/sticky-notes.js +170 -946
  128. package/lib/public/modules/tool-palette-order.js +143 -0
  129. package/lib/public/modules/tool-palette-overlays.js +159 -0
  130. package/lib/public/modules/tool-palette.js +21 -188
  131. package/lib/public/modules/tools.js +21 -1
  132. package/lib/public/modules/update-snooze.js +169 -0
  133. package/lib/public/modules/user-settings.js +4 -0
  134. package/lib/public/modules/worker-pane-lock.js +176 -0
  135. package/lib/public/modules/worker-proposal-state.js +16 -0
  136. package/lib/public/modules/worker-proposal.js +49 -12
  137. package/lib/public/style.css +3 -1
  138. package/lib/sdk-bridge.js +132 -46
  139. package/lib/sdk-message-processor.js +60 -14
  140. package/lib/server-experimental-settings.js +49 -0
  141. package/lib/server-global-ws.js +11 -0
  142. package/lib/server-home-chat-events.js +31 -1
  143. package/lib/server-home-chat.js +27 -4
  144. package/lib/server-home-clay-entry.js +1 -1
  145. package/lib/server-home-clay-session-links.js +34 -5
  146. package/lib/server-settings.js +5 -0
  147. package/lib/server-tools.js +127 -9
  148. package/lib/server.js +124 -18
  149. package/lib/session-driver-eligibility.js +38 -0
  150. package/lib/session-driver-orchestration.js +31 -0
  151. package/lib/session-hygiene.js +3 -0
  152. package/lib/session-notes-mcp-server.js +25 -5
  153. package/lib/session-pair-factory.js +247 -0
  154. package/lib/session-pair-mcp-server.js +46 -8
  155. package/lib/session-pair-prompts.js +79 -0
  156. package/lib/session-pair-turn-control.js +153 -0
  157. package/lib/session-provenance.js +119 -0
  158. package/lib/session-spawn-mcp-server.js +1 -1
  159. package/lib/session-split-groups.js +4 -1
  160. package/lib/session-title-policy.js +60 -0
  161. package/lib/session-visibility.js +39 -0
  162. package/lib/sessions.js +61 -11
  163. package/lib/tools-registry.js +89 -10
  164. package/lib/update-snooze.js +437 -0
  165. package/lib/users-experimental-preferences.js +30 -0
  166. package/lib/users-permissions.js +2 -2
  167. package/lib/users.js +22 -16
  168. package/lib/workspace-query-access.js +102 -0
  169. package/lib/workspace-query-service.js +24 -17
  170. package/lib/worktree.js +25 -38
  171. package/lib/ws-schema.js +38 -7
  172. package/lib/yoke/adapters/claude-worker.js +16 -6
  173. package/lib/yoke/adapters/claude.js +81 -4
  174. package/lib/yoke/adapters/codex.js +63 -15
  175. package/lib/yoke/codex-background-tasks.js +8 -2
  176. package/lib/yoke/vendor-registry.js +1 -1
  177. package/package.json +3 -3
@@ -0,0 +1,400 @@
1
+ // Project Logs message handling and session-bound MCP wiring.
2
+ //
3
+ // Every capability here is derived server-side. The WebSocket path binds from
4
+ // ws._clayUser plus this project's own slug, and the MCP path binds from the
5
+ // exact live session object. Neither accepts identity or project scope from
6
+ // the message, so a client cannot widen its own reach by asking.
7
+ //
8
+ // Authorship is split deliberately. Canonical entries are created and revised
9
+ // only by this project's agent sessions through `clay-logs`; a connected human
10
+ // reads the ledger, appends attributed comments, and lets the project owner
11
+ // delete an entry. That keeps the log an honest record rather than a wiki.
12
+
13
+ var logsMcp = require("./project-logs-mcp-server");
14
+
15
+ var LIST_LIMIT = 50;
16
+ var MAX_NOTICE_TITLE_CHARS = 160;
17
+ var MAX_NOTICE_SUMMARY_CHARS = 240;
18
+
19
+ // Canonical writes that move the ledger forward. A plain comment, a
20
+ // clarification, and a decline are participation, not revisions, so they never
21
+ // notify.
22
+ var NOTIFYING_OPS = ["create", "update", "link", "incorporate", "revert", "delete"];
23
+
24
+ function clip(value, max) {
25
+ if (typeof value !== "string") return "";
26
+ return value.length > max ? value.substring(0, max) : value;
27
+ }
28
+
29
+ // Bounded ledger metadata only. No body, no comment text, no author identity
30
+ // beyond the vendor that wrote it.
31
+ function updateNotice(entry, op) {
32
+ return {
33
+ type: "project_log_updated",
34
+ ref: entry.ref,
35
+ revision: entry.revisions,
36
+ op: op,
37
+ category: entry.category,
38
+ priority: entry.priority,
39
+ title: clip(entry.title, MAX_NOTICE_TITLE_CHARS),
40
+ summary: clip(entry.summary, MAX_NOTICE_SUMMARY_CHARS),
41
+ at: entry.updatedAt,
42
+ vendor: (entry.updatedBy && entry.updatedBy.vendor) || null,
43
+ };
44
+ }
45
+
46
+ var SYSTEM_PROMPT_LABEL = "--- Project Logs (durable project record; manage via clay-logs tools) ---";
47
+
48
+ function parseJsonArray(value) {
49
+ if (Array.isArray(value)) return value;
50
+ if (typeof value !== "string" || !value.trim()) return undefined;
51
+ var parsed;
52
+ try {
53
+ parsed = JSON.parse(value);
54
+ } catch (e) {
55
+ throw new Error("Expected a JSON array.");
56
+ }
57
+ if (!Array.isArray(parsed)) throw new Error("Expected a JSON array.");
58
+ return parsed;
59
+ }
60
+
61
+ // The tool surface accepts JSON-encoded arrays because MCP shapes are flat
62
+ // scalars. The service and store only ever see real arrays.
63
+ function coerceToolArgs(args) {
64
+ var out = Object.assign({}, args || {});
65
+ if (out.tags !== undefined) out.tags = parseJsonArray(out.tags);
66
+ if (out.links !== undefined) out.links = parseJsonArray(out.links);
67
+ return out;
68
+ }
69
+
70
+ var HANDLED_TYPES = [
71
+ "project_logs_list",
72
+ "project_log_read",
73
+ "project_log_comment",
74
+ "project_log_delete",
75
+ // Retired human mutations. Still claimed so an older client gets an
76
+ // explicit refusal rather than silence.
77
+ "project_log_create",
78
+ "project_log_update",
79
+ ];
80
+
81
+ var RETIRED_TYPES = ["project_log_create", "project_log_update"];
82
+
83
+ function attachProjectLogs(ctx) {
84
+ var service = ctx.service;
85
+ var sm = ctx.sm;
86
+ var slug = ctx.projectSlug;
87
+ var isMate = ctx.isMate === true;
88
+ var mateId = ctx.mateId || null;
89
+ var sendTo = ctx.sendTo;
90
+ var getClients = ctx.getClients || function () { return []; };
91
+ var getProjectOwnerId = ctx.getProjectOwnerId || function () { return null; };
92
+ var onFeedback = ctx.onFeedback || function () {};
93
+
94
+ // Every socket in this project's client set passed the project access check
95
+ // at WebSocket upgrade, so a broadcast here is already scoped to authorized
96
+ // human users of this project. Nothing is read from any payload. Pane
97
+ // connections are excluded, matching how sticky notes broadcast.
98
+ function broadcastUpdate(entry, op) {
99
+ if (isMate || !entry) return;
100
+ if (NOTIFYING_OPS.indexOf(op) === -1) return;
101
+ var notice = updateNotice(entry, op);
102
+ var clients = getClients();
103
+ for (var client of clients) {
104
+ if (!client || client.readyState !== 1 || client._clayPane) continue;
105
+ sendTo(client, notice);
106
+ }
107
+ }
108
+
109
+ function broadcastCommentReview(entry, request) {
110
+ if (isMate || !entry || !request || !request.commentId) return;
111
+ var notice = {
112
+ type: "project_log_comment_reviewed",
113
+ ref: entry.ref,
114
+ commentId: request.commentId,
115
+ action: request.action,
116
+ };
117
+ var clients = getClients();
118
+ for (var client of clients) {
119
+ if (!client || client.readyState !== 1 || client._clayPane) continue;
120
+ sendTo(client, notice);
121
+ }
122
+ }
123
+
124
+ // Wrap a canonical write so a successful one notifies. A throw never reaches
125
+ // here, so a failed or no-op write is silent.
126
+ function notifying(op, run) {
127
+ return function (args) {
128
+ var result = run(args);
129
+ broadcastUpdate(result, op);
130
+ return result;
131
+ };
132
+ }
133
+
134
+ // --- Bindings --------------------------------------------------------
135
+
136
+ function sessionBinding(session) {
137
+ if (!service) return null;
138
+ if (isMate) {
139
+ if (!mateId) return null;
140
+ if (session && (!sm || !sm.sessions || sm.sessions.get(session.localId) !== session)) return null;
141
+ return service.bindMate({
142
+ projectSlug: slug,
143
+ projectOwnerId: getProjectOwnerId(),
144
+ isMate: true,
145
+ mateId: mateId,
146
+ session: session || null,
147
+ });
148
+ }
149
+ if (!session) return null;
150
+ return service.bindProjectSession({ projectSlug: slug, session: session });
151
+ }
152
+
153
+ function userBinding(ws) {
154
+ if (!service || isMate) return null;
155
+ return service.bindUser({ projectSlug: slug, user: (ws && ws._clayUser) || null });
156
+ }
157
+
158
+ // --- WebSocket surface -----------------------------------------------
159
+
160
+ function fail(ws, requestId, message) {
161
+ sendTo(ws, { type: "project_logs_error", requestId: requestId || null, message: message });
162
+ }
163
+
164
+ // Search already returns the ledger row shape, so a query needs no per-hit
165
+ // read and neither path dumps a record body into the list.
166
+ // Returns both the rows and this project's live category vocabulary, so the
167
+ // filter is populated solely from what this project actually uses.
168
+ function listEntries(bound, query, category, contextMode) {
169
+ var options = { kind: category, limit: LIST_LIMIT, contextMode: contextMode };
170
+ if (query) {
171
+ options.query = query;
172
+ var hits = bound.searchLogs(options);
173
+ return {
174
+ entries: hits.results,
175
+ categories: hits.categories || [],
176
+ contextMode: hits.contextMode,
177
+ currentChangeSetId: hits.currentChangeSetId,
178
+ isWorktree: hits.isWorktree,
179
+ };
180
+ }
181
+ var listed = bound.listLogs(options);
182
+ return {
183
+ entries: listed.entries,
184
+ categories: listed.categories || [],
185
+ contextMode: listed.contextMode,
186
+ currentChangeSetId: listed.currentChangeSetId,
187
+ isWorktree: listed.isWorktree,
188
+ };
189
+ }
190
+
191
+ function handleLogsMessage(ws, msg) {
192
+ if (!msg || typeof msg.type !== "string") return false;
193
+ if (HANDLED_TYPES.indexOf(msg.type) === -1) return false;
194
+
195
+ var requestId = typeof msg.requestId === "string" ? msg.requestId : null;
196
+ if (isMate) {
197
+ fail(ws, requestId, "Project Logs are available in projects, not in Mate conversations.");
198
+ return true;
199
+ }
200
+
201
+ // Canonical authorship belongs to the project's agent sessions. A human
202
+ // asking to create or revise is refused before any binding is attempted,
203
+ // so no privilege level can reach the canonical writers.
204
+ if (RETIRED_TYPES.indexOf(msg.type) !== -1) {
205
+ fail(ws, requestId, "Project Logs are written by this project's agent sessions. Add a comment instead.");
206
+ return true;
207
+ }
208
+
209
+ var bound = userBinding(ws);
210
+ if (!bound) {
211
+ fail(ws, requestId, "Project Logs are not available for this project.");
212
+ return true;
213
+ }
214
+
215
+ try {
216
+ if (msg.type === "project_logs_list") {
217
+ var query = typeof msg.query === "string" ? msg.query.trim() : "";
218
+ // A view preference, not an authorization input: the store validates it.
219
+ var category = typeof msg.category === "string" && msg.category ? msg.category : null;
220
+ var contextMode = typeof msg.contextMode === "string" ? msg.contextMode : null;
221
+ var listing = listEntries(bound, query, category, contextMode);
222
+ sendTo(ws, {
223
+ type: "project_logs_state",
224
+ requestId: requestId,
225
+ entries: listing.entries,
226
+ categories: listing.categories,
227
+ contextMode: listing.contextMode,
228
+ currentChangeSetId: listing.currentChangeSetId,
229
+ isWorktree: listing.isWorktree === true,
230
+ canDelete: bound.canDelete === true,
231
+ });
232
+ return true;
233
+ }
234
+
235
+ if (msg.type === "project_log_read") {
236
+ sendTo(ws, { type: "project_log_entry", requestId: requestId, entry: bound.readLog({ ref: msg.ref }), canDelete: bound.canDelete === true });
237
+ return true;
238
+ }
239
+
240
+ if (msg.type === "project_log_delete") {
241
+ var removed = bound.removeLog({ ref: msg.ref });
242
+ sendTo(ws, { type: "project_log_deleted", requestId: requestId, ref: removed.ref });
243
+ broadcastUpdate(removed, "delete");
244
+ return true;
245
+ }
246
+
247
+ if (msg.type === "project_log_comment") {
248
+ var commented = bound.commentLog({ ref: msg.ref, body: msg.body });
249
+ var reviewQueued = false;
250
+ try { reviewQueued = onFeedback(commented) === true; } catch (deliveryError) {
251
+ console.error("[project-logs] Failed to deliver comment:", deliveryError.message || deliveryError);
252
+ }
253
+ sendTo(ws, { type: "project_log_commented", requestId: requestId, entry: commented, reviewQueued: reviewQueued });
254
+ return true;
255
+ }
256
+ } catch (e) {
257
+ fail(ws, requestId, (e && e.message) || "Project Logs could not complete the request.");
258
+ return true;
259
+ }
260
+
261
+ return false;
262
+ }
263
+
264
+ // --- MCP surface ------------------------------------------------------
265
+
266
+ function toolHandlers(bound) {
267
+ if (!bound) return null;
268
+ return {
269
+ listLogs: function (args) { return bound.listLogs(args || {}); },
270
+ searchLogs: function (args) { return bound.searchLogs(args || {}); },
271
+ readLog: function (args) { return bound.readLog(args || {}); },
272
+ logHistory: function (args) { return bound.logHistory(args || {}); },
273
+ readLogRevision: function (args) { return bound.readLogRevision(args || {}); },
274
+ listLogFeedback: function (args) { return bound.listLogFeedback(args || {}); },
275
+ reviewLogComment: function (args) {
276
+ var request = coerceToolArgs(args);
277
+ var result = bound.reviewLogComment(request);
278
+ broadcastCommentReview(result, request);
279
+ // Only an incorporation is a canonical revision; clarify and decline
280
+ // resolve a comment and change nothing.
281
+ if (request && request.action === "incorporate") broadcastUpdate(result, "incorporate");
282
+ return result;
283
+ },
284
+ revertLog: notifying("revert", function (args) { return bound.revertLog(args || {}); }),
285
+ createLog: notifying("create", function (args) { return bound.createLog(coerceToolArgs(args)); }),
286
+ updateLog: notifying("update", function (args) { return bound.updateLog(coerceToolArgs(args)); }),
287
+ linkLog: notifying("link", function (args) { return bound.linkLog(coerceToolArgs(args)); }),
288
+ };
289
+ }
290
+
291
+ // A Mate project advertises tools only when the registry confirms
292
+ // authoritative builtin Clay, and then only the read-only cross-project set.
293
+ function includeGlobal() {
294
+ if (!isMate) return false;
295
+ var projectBound = sessionBinding(null);
296
+ return !!(projectBound && projectBound.isClay === true);
297
+ }
298
+
299
+ function getToolDefs(session) {
300
+ if (!service) return [];
301
+ if (isMate && !includeGlobal()) return [];
302
+ var bound = session ? sessionBinding(session) : null;
303
+ if (session && !bound) return [];
304
+ return logsMcp.getToolDefs(toolHandlers(bound), isMate);
305
+ }
306
+
307
+ function createMcpServer(adapter, session) {
308
+ if (!service) return null;
309
+ if (isMate && !includeGlobal()) return null;
310
+ if (!isMate && session && !sessionBinding(session)) {
311
+ // A session that cannot bind still gets the fail-closed descriptor
312
+ // rather than a silently missing server.
313
+ return logsMcp.createMcpServer(adapter, null, false);
314
+ }
315
+ var bound = session ? sessionBinding(session) : null;
316
+ return logsMcp.createMcpServer(adapter, toolHandlers(bound), isMate);
317
+ }
318
+
319
+ function getDynamicToolDefs(session) {
320
+ return getToolDefs(session).map(function (tool) {
321
+ tool.permissionName = "mcp__clay-logs__" + tool.name;
322
+ return tool;
323
+ });
324
+ }
325
+
326
+ function getBridgeTools(session, normalizeSchema) {
327
+ if (!session) return [];
328
+ var defs = getToolDefs(session);
329
+ var tools = [];
330
+ for (var i = 0; i < defs.length; i++) {
331
+ tools.push({
332
+ server: "clay-logs",
333
+ name: defs[i].name,
334
+ description: defs[i].description || defs[i].name,
335
+ inputSchema: normalizeSchema(defs[i].inputSchema),
336
+ });
337
+ }
338
+ return tools;
339
+ }
340
+
341
+ function callBridgeTool(session, toolName, args) {
342
+ if (!session) return Promise.reject(new Error("Project Logs require a valid session."));
343
+ var defs = getToolDefs(session);
344
+ for (var i = 0; i < defs.length; i++) {
345
+ if (defs[i].name === toolName) return Promise.resolve(defs[i].handler(args || {}));
346
+ }
347
+ return Promise.reject(new Error("Project Logs tool not found: " + toolName));
348
+ }
349
+
350
+ // A count only. The comment bodies stay in the tool, so a pending queue is
351
+ // discoverable on the next turn without dumping the discussion into context
352
+ // and without starting a turn of its own.
353
+ function pendingFeedbackSignal(session) {
354
+ if (!session) return "";
355
+ var bound = sessionBinding(session);
356
+ if (!bound || typeof bound.listLogFeedback !== "function") return "";
357
+ var total = 0;
358
+ try {
359
+ total = bound.listLogFeedback({ limit: 1 }).total || 0;
360
+ } catch (e) {
361
+ return "";
362
+ }
363
+ if (!total) return "";
364
+ return "\n" + total + (total === 1 ? " comment awaits" : " comments await") +
365
+ " your review. Call list_log_feedback to see them.";
366
+ }
367
+
368
+ function getSystemPrompt(session) {
369
+ if (!service) return "";
370
+ if (isMate) {
371
+ if (!includeGlobal() || (session && !sessionBinding(session))) return "";
372
+ return SYSTEM_PROMPT_LABEL + "\n" + logsMcp.CLAY_READ_CONTRACT;
373
+ }
374
+ if (session && !sessionBinding(session)) return "";
375
+ return SYSTEM_PROMPT_LABEL + "\n" + logsMcp.LOGS_CONTRACT +
376
+ "\n" + logsMcp.LEARNING_CONTRACT +
377
+ "\n" + logsMcp.ATTENTION_CONTRACT +
378
+ "\n" + logsMcp.REVIEW_CONTRACT + pendingFeedbackSignal(session);
379
+ }
380
+
381
+ return {
382
+ handleLogsMessage: handleLogsMessage,
383
+ createMcpServer: createMcpServer,
384
+ getToolDefs: getToolDefs,
385
+ getDynamicToolDefs: getDynamicToolDefs,
386
+ getBridgeTools: getBridgeTools,
387
+ callBridgeTool: callBridgeTool,
388
+ getSystemPrompt: getSystemPrompt,
389
+ };
390
+ }
391
+
392
+ module.exports = {
393
+ SYSTEM_PROMPT_LABEL: SYSTEM_PROMPT_LABEL,
394
+ NOTIFYING_OPS: NOTIFYING_OPS,
395
+ updateNotice: updateNotice,
396
+ HANDLED_TYPES: HANDLED_TYPES,
397
+ RETIRED_TYPES: RETIRED_TYPES,
398
+ attachProjectLogs: attachProjectLogs,
399
+ coerceToolArgs: coerceToolArgs,
400
+ };
@@ -1,6 +1,18 @@
1
1
  var fs = require("fs");
2
2
  var path = require("path");
3
3
  var crypto = require("crypto");
4
+ var mateSync = require("./mate-knowledge-sync");
5
+
6
+ // Mirror a Mate-owned journal into the Clay-wide Knowledge backend after the
7
+ // legacy append has already succeeded. Reconciling the whole journal keeps the
8
+ // new scope current whether the write added, rewrote, or removed lines.
9
+ function mirrorMateJournal(mateDir, fileName, job) {
10
+ mateSync.syncQuietly({
11
+ mateDir: mateDir,
12
+ fileName: fileName,
13
+ actor: { type: "agent", vendor: (job && job.vendor) || null, sessionKey: (job && job.sessionKey) || null },
14
+ });
15
+ }
4
16
 
5
17
  function attachMateInteraction(ctx) {
6
18
  var cwd = ctx.cwd;
@@ -278,6 +290,7 @@ function attachMateInteraction(ctx) {
278
290
  fs.mkdirSync(knowledgeDir, { recursive: true });
279
291
  var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
280
292
  fs.appendFileSync(digestFile, JSON.stringify(digestObj) + "\n");
293
+ mirrorMateJournal(mateDir, "session-digests.jsonl", job);
281
294
  } catch (e) {
282
295
  console.error("[digest-worker] Write failed for " + job.mateId + ":", e.message);
283
296
  }
@@ -302,6 +315,7 @@ function attachMateInteraction(ctx) {
302
315
  }));
303
316
  }
304
317
  fs.appendFileSync(obsFile, obsLines.join("\n") + "\n");
318
+ mirrorMateJournal(mateDir, "user-observations.jsonl", job);
305
319
  } catch (e) {
306
320
  console.error("[digest-worker] Observations write failed for " + job.mateId + ":", e.message);
307
321
  }
@@ -0,0 +1,110 @@
1
+ // Session-bound `clay-knowledge` wiring for Mate projects.
2
+ //
3
+ // Mate Knowledge is available only inside a Mate project. A non-Mate Project
4
+ // Driver session gets nothing here and continues to use `clay-logs`. Which
5
+ // tool set a Mate receives is decided by the server Mate registry through the
6
+ // service binder, never by a project flag or a tool argument.
7
+
8
+ var knowledgeMcp = require("./mate-knowledge-mcp-server");
9
+
10
+ var SYSTEM_PROMPT_LABEL = "--- Your Knowledge (durable personal and expertise context; manage via clay-knowledge tools) ---";
11
+ var CLAY_PROMPT_LABEL = "--- Mate Knowledge coordination (via clay-knowledge tools) ---";
12
+
13
+ function attachProjectMateKnowledge(ctx) {
14
+ var service = ctx.service;
15
+ var sm = ctx.sm;
16
+ var slug = ctx.projectSlug;
17
+ var isMate = ctx.isMate === true;
18
+ var mateId = ctx.mateId || null;
19
+ var getProjectOwnerId = ctx.getProjectOwnerId || function () { return null; };
20
+
21
+ function binding(session) {
22
+ if (!service || !isMate || !mateId) return null;
23
+ if (session && (!sm || !sm.sessions || sm.sessions.get(session.localId) !== session)) return null;
24
+ return service.bind({
25
+ projectSlug: slug,
26
+ projectOwnerId: getProjectOwnerId(),
27
+ isMate: true,
28
+ mateId: mateId,
29
+ session: session || null,
30
+ });
31
+ }
32
+
33
+ // Decided once from the project-level binding so a static descriptor and a
34
+ // session-bound server always advertise the same tool set.
35
+ function includeClay() {
36
+ var projectBound = binding(null);
37
+ return !!(projectBound && projectBound.isClay === true);
38
+ }
39
+
40
+ function getToolDefs(session) {
41
+ if (!service || !isMate) return [];
42
+ var bound = session ? binding(session) : null;
43
+ if (session && !bound) return [];
44
+ return knowledgeMcp.getToolDefs(bound, includeClay());
45
+ }
46
+
47
+ function createMcpServer(adapter, session) {
48
+ if (!service || !isMate) return null;
49
+ if (!binding(null)) return null;
50
+ var bound = session ? binding(session) : null;
51
+ // A session that cannot bind still gets the fail-closed descriptor rather
52
+ // than a silently missing server.
53
+ return knowledgeMcp.createMcpServer(adapter, bound, includeClay());
54
+ }
55
+
56
+ function getDynamicToolDefs(session) {
57
+ return getToolDefs(session).map(function (tool) {
58
+ tool.permissionName = "mcp__clay-knowledge__" + tool.name;
59
+ return tool;
60
+ });
61
+ }
62
+
63
+ function getBridgeTools(session, normalizeSchema) {
64
+ if (!session) return [];
65
+ var defs = getToolDefs(session);
66
+ var tools = [];
67
+ for (var i = 0; i < defs.length; i++) {
68
+ tools.push({
69
+ server: "clay-knowledge",
70
+ name: defs[i].name,
71
+ description: defs[i].description || defs[i].name,
72
+ inputSchema: normalizeSchema(defs[i].inputSchema),
73
+ });
74
+ }
75
+ return tools;
76
+ }
77
+
78
+ function callBridgeTool(session, toolName, args) {
79
+ if (!session) return Promise.reject(new Error("Knowledge tools require a valid Mate session."));
80
+ var defs = getToolDefs(session);
81
+ for (var i = 0; i < defs.length; i++) {
82
+ if (defs[i].name === toolName) return Promise.resolve(defs[i].handler(args || {}));
83
+ }
84
+ return Promise.reject(new Error("Knowledge tool not found: " + toolName));
85
+ }
86
+
87
+ function getSystemPrompt(session) {
88
+ if (!service || !isMate) return "";
89
+ var bound = session ? binding(session) : binding(null);
90
+ if (!bound) return "";
91
+ return bound.isClay
92
+ ? CLAY_PROMPT_LABEL + "\n" + knowledgeMcp.CLAY_CONTRACT
93
+ : SYSTEM_PROMPT_LABEL + "\n" + knowledgeMcp.MATE_CONTRACT;
94
+ }
95
+
96
+ return {
97
+ createMcpServer: createMcpServer,
98
+ getToolDefs: getToolDefs,
99
+ getDynamicToolDefs: getDynamicToolDefs,
100
+ getBridgeTools: getBridgeTools,
101
+ callBridgeTool: callBridgeTool,
102
+ getSystemPrompt: getSystemPrompt,
103
+ };
104
+ }
105
+
106
+ module.exports = {
107
+ SYSTEM_PROMPT_LABEL: SYSTEM_PROMPT_LABEL,
108
+ CLAY_PROMPT_LABEL: CLAY_PROMPT_LABEL,
109
+ attachProjectMateKnowledge: attachProjectMateKnowledge,
110
+ };
@@ -1,5 +1,17 @@
1
1
  var fs = require("fs");
2
2
  var path = require("path");
3
+ var mateSync = require("./mate-knowledge-sync");
4
+
5
+ // Mirror a Mate-owned durable memory source into the Clay-wide Knowledge
6
+ // backend after the legacy write has already succeeded. Reconciling the source
7
+ // covers writes, rewrites, and removals with one call.
8
+ function mirrorMateSource(mateDir, fileName, actor) {
9
+ mateSync.syncQuietly({
10
+ mateDir: mateDir,
11
+ fileName: fileName,
12
+ actor: actor || { type: "agent" },
13
+ });
14
+ }
3
15
 
4
16
  function attachMemory(ctx) {
5
17
  var cwd = ctx.cwd;
@@ -467,6 +479,7 @@ function attachMemory(ctx) {
467
479
  }
468
480
  fs.mkdirSync(knowledgeDir, { recursive: true });
469
481
  fs.writeFileSync(summaryFile, cleaned + "\n", "utf8");
482
+ mirrorMateSource(path.dirname(knowledgeDir), "memory-summary.md");
470
483
  console.log("[memory-summary] Updated memory-summary.md for mate " + mateId);
471
484
  } catch (e) {
472
485
  console.error("[memory-summary] Failed to write memory-summary.md for mate " + mateId + ":", e.message);
@@ -573,6 +586,7 @@ function attachMemory(ctx) {
573
586
  }
574
587
  fs.mkdirSync(knowledgeDir, { recursive: true });
575
588
  fs.writeFileSync(summaryFile, cleaned + "\n", "utf8");
589
+ mirrorMateSource(path.dirname(knowledgeDir), "memory-summary.md");
576
590
  console.log("[memory-summary] Generated initial memory-summary.md for mate " + mateId + " from " + digestsText.length + " digests");
577
591
  } catch (e) {
578
592
  console.error("[memory-summary] Failed to write initial memory-summary.md for mate " + mateId + ":", e.message);
@@ -666,6 +680,14 @@ function attachMemory(ctx) {
666
680
  } else {
667
681
  fs.writeFileSync(digestFile, lines.join("\n") + "\n");
668
682
  }
683
+ // Reconciling the journal after the rewrite tombstones the removed
684
+ // line, so a deleted memory does not stay active in the new backend.
685
+ var deleteUser = (ws && ws._clayUser) || null;
686
+ mirrorMateSource(cwd, "session-digests.jsonl", {
687
+ type: deleteUser ? "user" : "system",
688
+ userId: deleteUser ? deleteUser.id : null,
689
+ displayName: deleteUser ? (deleteUser.displayName || deleteUser.username) : null,
690
+ });
669
691
  }
670
692
  } catch (e) {}
671
693
  sendTo(ws, { type: "memory_deleted", index: msg.index });
@@ -0,0 +1,70 @@
1
+ // Idempotent acknowledgement support for ordinary project chat messages.
2
+
3
+ var CLIENT_MESSAGE_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
4
+
5
+ function normalizeClientMessageId(value) {
6
+ if (typeof value !== "string" || !CLIENT_MESSAGE_ID_RE.test(value)) return null;
7
+ return value;
8
+ }
9
+
10
+ function ownerKey(ws) {
11
+ return ws && ws._clayUser && ws._clayUser.id ? String(ws._clayUser.id) : "_default";
12
+ }
13
+
14
+ function deliveryKey(ownerId, clientMessageId) {
15
+ return ownerId + ":" + clientMessageId;
16
+ }
17
+
18
+ function ensureRecordedMessages(session) {
19
+ if (session._recordedClientMessages) return session._recordedClientMessages;
20
+ var recorded = new Map();
21
+ var history = Array.isArray(session.history) ? session.history : [];
22
+ for (var i = 0; i < history.length; i++) {
23
+ var item = history[i];
24
+ var clientMessageId = normalizeClientMessageId(item && item.clientMessageId);
25
+ if (!clientMessageId) continue;
26
+ recorded.set(deliveryKey(item.from ? String(item.from) : "_default", clientMessageId), item);
27
+ }
28
+ session._recordedClientMessages = recorded;
29
+ return recorded;
30
+ }
31
+
32
+ function createProjectMessageDelivery(sendTo) {
33
+ function inspect(ws, session, msg) {
34
+ var clientMessageId = normalizeClientMessageId(msg && msg.clientMessageId);
35
+ if (!clientMessageId) return { clientMessageId: null, ownerId: ownerKey(ws), duplicate: false };
36
+ var ownerId = ownerKey(ws);
37
+ var recordedMessage = ensureRecordedMessages(session).get(deliveryKey(ownerId, clientMessageId)) || null;
38
+ return {
39
+ clientMessageId: clientMessageId,
40
+ ownerId: ownerId,
41
+ duplicate: !!recordedMessage,
42
+ recordedMessage: recordedMessage,
43
+ };
44
+ }
45
+
46
+ function markRecorded(session, receipt, message) {
47
+ if (!receipt || !receipt.clientMessageId) return;
48
+ ensureRecordedMessages(session).set(deliveryKey(receipt.ownerId, receipt.clientMessageId), message);
49
+ }
50
+
51
+ function acknowledge(ws, receipt, message) {
52
+ if (!receipt || !receipt.clientMessageId) return;
53
+ sendTo(ws, {
54
+ type: "message_ack",
55
+ clientMessageId: receipt.clientMessageId,
56
+ message: message || receipt.recordedMessage || null,
57
+ });
58
+ }
59
+
60
+ return {
61
+ inspect: inspect,
62
+ markRecorded: markRecorded,
63
+ acknowledge: acknowledge,
64
+ };
65
+ }
66
+
67
+ module.exports = {
68
+ createProjectMessageDelivery: createProjectMessageDelivery,
69
+ normalizeClientMessageId: normalizeClientMessageId,
70
+ };