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
@@ -1,5 +1,6 @@
1
1
  var sessionSearch = require("./session-search");
2
2
  var builtinMates = require("./builtin-mates");
3
+ var workspaceQueryAccess = require("./workspace-query-access");
3
4
  var crypto = require("crypto");
4
5
 
5
6
  var MAX_PAGE = 50;
@@ -100,7 +101,13 @@ function attachWorkspaceQueryService(ctx) {
100
101
  var getProjects = ctx.getProjects;
101
102
  var isMultiUser = ctx.isMultiUser;
102
103
  var resolveMate = ctx.resolveMate;
103
- var canAccessProject = ctx.canAccessProject;
104
+ // Ownership and access both come from here, never from getStatus().
105
+ var projectAccess = workspaceQueryAccess.attachWorkspaceQueryAccess({
106
+ isMultiUser: isMultiUser,
107
+ resolveMate: resolveMate,
108
+ onGetProjectAccess: ctx.onGetProjectAccess,
109
+ canAccessProject: ctx.canAccessProject,
110
+ });
104
111
 
105
112
  function authoritativeMate(source) {
106
113
  if (!source || source.isMate !== true || !source.mateId || typeof resolveMate !== "function") return null;
@@ -164,12 +171,6 @@ function attachWorkspaceQueryService(ctx) {
164
171
  });
165
172
  }
166
173
 
167
- function ownsProject(principal, status) {
168
- if (!status || status.isWorktree) return false;
169
- if (isMultiUser()) return !!principal.userId && status.projectOwnerId === principal.userId;
170
- return !status.projectOwnerId || (!!principal.singleUserOwnerId && status.projectOwnerId === principal.singleUserOwnerId);
171
- }
172
-
173
174
  function ownsSession(principal, session) {
174
175
  if (!session || session.hidden) return false;
175
176
  if (isMultiUser()) return !!principal.userId && session.ownerId === principal.userId;
@@ -180,12 +181,12 @@ function attachWorkspaceQueryService(ctx) {
180
181
  var projects = [];
181
182
  getProjects().forEach(function (project, slug) {
182
183
  var status = project.getStatus();
183
- var projectOwned = ownsProject(principal, status);
184
- var projectAccessible = projectOwned;
185
- if (!projectAccessible && isMultiUser() && typeof canAccessProject === "function") {
186
- projectAccessible = canAccessProject(principal.userId, status) === true;
187
- }
188
- if (!projectAccessible || (status && status.isWorktree)) return;
184
+ if (!status || status.isWorktree) return;
185
+ // Authorized here rather than at bind time, so an already-created tool
186
+ // handler loses a project the moment its access record changes.
187
+ var verdict = projectAccess.evaluate(principal, status);
188
+ if (!verdict.accessible) return;
189
+ var projectOwned = verdict.owned;
189
190
  var manager = project.getSessionManager ? project.getSessionManager() : project.sm;
190
191
  if (!manager || !manager.sessions) return;
191
192
  var sessions = [];
@@ -305,14 +306,20 @@ function attachWorkspaceQueryService(ctx) {
305
306
  function searchHistory(principal, projectSlug, args) {
306
307
  var query = cleanText(args && args.query || "", 500);
307
308
  if (!query) throw new Error("A search query is required.");
308
- var projects;
309
+ // One authorization snapshot for the whole call. Resolving hits back to
310
+ // their project through findProject() would re-authorize every project once
311
+ // per hit, so the visible set is captured once and indexed.
312
+ var visible;
309
313
  if (projectSlug) {
310
314
  var project = findProject(principal, projectSlug);
311
315
  if (!project) throw new Error("Project not found in the owned workspace.");
312
- projects = [sanitizedProject(project)];
316
+ visible = [project];
313
317
  } else {
314
- projects = collect(principal).map(sanitizedProject);
318
+ visible = collect(principal);
315
319
  }
320
+ var bySlug = {};
321
+ for (var v = 0; v < visible.length; v++) bySlug[visible[v].projectSlug] = visible[v];
322
+ var projects = visible.map(sanitizedProject);
316
323
  var max = Math.min(100, decodeCursor(args && args.cursor) + parseLimit(args && args.limit, 20) + 20);
317
324
  var ranked = sessionSearch.searchPalette(projects, query, { maxResults: max });
318
325
  var offset = decodeCursor(args && args.cursor);
@@ -321,7 +328,7 @@ function attachWorkspaceQueryService(ctx) {
321
328
  var results = [];
322
329
  for (var i = 0; i < selected.length; i++) {
323
330
  var hit = selected[i];
324
- var projectForHit = findProject(principal, hit.projectSlug);
331
+ var projectForHit = bySlug[hit.projectSlug] || null;
325
332
  var session = projectForHit ? findSession(projectForHit, String(hit.sessionId)) : null;
326
333
  if (!session) continue;
327
334
  results.push({
package/lib/worktree.js CHANGED
@@ -1,6 +1,6 @@
1
- var { execFileSync } = require("child_process");
2
1
  var fs = require("fs");
3
2
  var path = require("path");
3
+ var { runGitSync } = require("./git-cli");
4
4
 
5
5
  // Parse `git worktree list --porcelain` output into structured objects
6
6
  function parseWorktreeOutput(output) {
@@ -28,14 +28,10 @@ function parseWorktreeOutput(output) {
28
28
  }
29
29
 
30
30
  // Check if a given path is itself a worktree (not the main working tree)
31
- function isWorktree(projectPath) {
31
+ function isWorktree(projectPath, osUserInfo) {
32
32
  try {
33
- var gitDir = execFileSync("git", ["rev-parse", "--git-dir"], {
34
- cwd: projectPath, encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
35
- }).trim();
36
- var commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
37
- cwd: projectPath, encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
38
- }).trim();
33
+ var gitDir = runGitSync(projectPath, ["rev-parse", "--git-dir"], null, osUserInfo).trim();
34
+ var commonDir = runGitSync(projectPath, ["rev-parse", "--git-common-dir"], null, osUserInfo).trim();
39
35
  var absGit = path.resolve(projectPath, gitDir);
40
36
  var absCommon = path.resolve(projectPath, commonDir);
41
37
  return absGit !== absCommon;
@@ -49,18 +45,21 @@ function isPathInside(parentPath, candidatePath) {
49
45
  return relative !== "" && relative !== ".." && relative.indexOf(".." + path.sep) !== 0 && !path.isAbsolute(relative);
50
46
  }
51
47
 
48
+ function worktreePath(projectPath, dirName) {
49
+ if (typeof dirName !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(dirName)) return null;
50
+ var resolvedParent = path.resolve(projectPath);
51
+ var resolvedWorktree = path.resolve(resolvedParent, dirName);
52
+ return isPathInside(resolvedParent, resolvedWorktree) ? resolvedWorktree : null;
53
+ }
54
+
52
55
  // Scan worktrees for a given project path
53
56
  // Returns array of { path, branch, bare, detached, external }
54
57
  // external = true when Git registered the worktree outside the main project folder
55
- function scanWorktrees(projectPath) {
58
+ function scanWorktrees(projectPath, osUserInfo) {
56
59
  var resolvedParent = path.resolve(projectPath);
60
+ try { resolvedParent = fs.realpathSync(resolvedParent); } catch (e) {}
57
61
  try {
58
- var output = execFileSync("git", ["worktree", "list", "--porcelain"], {
59
- cwd: resolvedParent,
60
- encoding: "utf8",
61
- timeout: 5000,
62
- stdio: ["pipe", "pipe", "pipe"],
63
- });
62
+ var output = runGitSync(resolvedParent, ["worktree", "list", "--porcelain"], null, osUserInfo);
64
63
  var all = parseWorktreeOutput(output);
65
64
  // Filter out bare worktrees and the main worktree itself
66
65
  var results = [];
@@ -68,10 +67,11 @@ function scanWorktrees(projectPath) {
68
67
  var wt = all[i];
69
68
  if (wt.bare) continue;
70
69
  var resolvedWt = path.resolve(wt.path);
71
- if (resolvedWt === resolvedParent) continue;
72
70
  // Git retains removed worktrees until `git worktree prune` runs. Do not
73
71
  // register those stale paths as Clay projects.
74
72
  if (!fs.existsSync(resolvedWt)) continue;
73
+ try { resolvedWt = fs.realpathSync(resolvedWt); } catch (e) {}
74
+ if (resolvedWt === resolvedParent) continue;
75
75
  wt.external = !isPathInside(resolvedParent, resolvedWt);
76
76
  wt.dirName = path.basename(wt.path);
77
77
  results.push(wt);
@@ -84,28 +84,19 @@ function scanWorktrees(projectPath) {
84
84
 
85
85
  // Create a new worktree inside the parent project directory
86
86
  // Returns { ok, path, error }
87
- function createWorktree(projectPath, branchName, dirName, baseBranch) {
87
+ function createWorktree(projectPath, branchName, dirName, baseBranch, osUserInfo) {
88
88
  var resolvedParent = path.resolve(projectPath);
89
- var wtPath = path.join(resolvedParent, dirName || branchName);
89
+ var wtPath = worktreePath(resolvedParent, dirName || branchName);
90
+ if (!wtPath) return { ok: false, error: "Invalid worktree directory name" };
90
91
  var base = baseBranch || "main";
91
92
  // Try creating with -b (new branch)
92
93
  try {
93
- execFileSync("git", ["worktree", "add", wtPath, "-b", branchName, base], {
94
- cwd: resolvedParent,
95
- encoding: "utf8",
96
- timeout: 15000,
97
- stdio: ["pipe", "pipe", "pipe"],
98
- });
94
+ runGitSync(resolvedParent, ["worktree", "add", wtPath, "-b", branchName, base], { timeout: 15000 }, osUserInfo);
99
95
  return { ok: true, path: wtPath };
100
96
  } catch (e) {
101
97
  // Branch may already exist, try without -b
102
98
  try {
103
- execFileSync("git", ["worktree", "add", wtPath, branchName], {
104
- cwd: resolvedParent,
105
- encoding: "utf8",
106
- timeout: 15000,
107
- stdio: ["pipe", "pipe", "pipe"],
108
- });
99
+ runGitSync(resolvedParent, ["worktree", "add", wtPath, branchName], { timeout: 15000 }, osUserInfo);
109
100
  return { ok: true, path: wtPath };
110
101
  } catch (e2) {
111
102
  return { ok: false, error: e2.message || "Failed to create worktree" };
@@ -115,17 +106,13 @@ function createWorktree(projectPath, branchName, dirName, baseBranch) {
115
106
 
116
107
  // Remove a worktree
117
108
  // Returns { ok, error }
118
- function removeWorktree(projectPath, worktreeDirName) {
109
+ function removeWorktree(projectPath, worktreeDirName, osUserInfo) {
119
110
  var resolvedParent = path.resolve(projectPath);
120
- var wtPath = path.join(resolvedParent, worktreeDirName);
111
+ var wtPath = worktreePath(resolvedParent, worktreeDirName);
112
+ if (!wtPath) return { ok: false, error: "Invalid worktree directory name" };
121
113
  // Try normal remove first
122
114
  try {
123
- execFileSync("git", ["worktree", "remove", wtPath], {
124
- cwd: resolvedParent,
125
- encoding: "utf8",
126
- timeout: 15000,
127
- stdio: ["pipe", "pipe", "pipe"],
128
- });
115
+ runGitSync(resolvedParent, ["worktree", "remove", wtPath], { timeout: 15000 }, osUserInfo);
129
116
  return { ok: true };
130
117
  } catch (e) {
131
118
  var errMsg = (e.stderr || e.message || "").toString();
package/lib/ws-schema.js CHANGED
@@ -40,10 +40,12 @@ var schema = {
40
40
  "split_group_set_pair": { direction: "c2s", handler: "lib/session-split-groups.js", description: "Assign Driver/Worker roles on a split group (driverId null clears them)" },
41
41
  "pair_session_options": { direction: "c2s", handler: "lib/project-session-pair.js", description: "Request vendors/models for the pair-session dialog (response reuses the same type)" },
42
42
  "pair_session_create": { direction: "c2s", handler: "lib/project-session-pair.js", description: "Create a Driver/Split Worker session pair as a split group" },
43
- "worker_proposal_response": { direction: "c2s", handler: "lib/project-worker-proposal.js", description: "Accept or decline a Fable Worker suggestion with the selected runtime" },
43
+ "worker_proposal_response": { direction: "c2s", handler: "lib/project-worker-proposal.js", description: "Accept or decline an exact Driver Split Worker configuration with the selected runtime" },
44
44
  "project_assignment_response": { direction: "c2s", handler: "lib/workspace-assignment-service.js", description: "Approve or cancel an exact-session cross-project assignment proposal" },
45
45
  "home_clay_session_resolve": { direction: "c2s", handler: "lib/server-home-chat.js", description: "Resolve an opaque Mate session reference through the exact bound source session" },
46
46
  "home_clay_session_target": { direction: "s2c", handler: "lib/public/modules/command-palette.js", description: "Return an owner-validated navigation target for a rendered Mate session reference" },
47
+ "home_clay_log_resolve": { direction: "c2s", handler: "lib/server-home-chat.js", description: "Resolve an opaque Project Log reference through the exact bound Clay session" },
48
+ "home_clay_log_target": { direction: "s2c", handler: "lib/public/modules/command-palette.js", description: "Return an owner-validated project target for a rendered Log reference" },
47
49
 
48
50
  "session_list": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Full list of sessions for the sidebar" },
49
51
  "session_switched": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Confirmation that active session changed" },
@@ -62,8 +64,8 @@ var schema = {
62
64
  "split_groups": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Full persistent split-group list for the current user" },
63
65
  "split_group_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Result of a split-group mutation" },
64
66
  "pair_session_created": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Result of pair_session_create; carries the new group" },
65
- "worker_proposal": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Inline Fable suggestion to delegate execution to a Worker" },
66
- "worker_proposal_update": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Worker suggestion lifecycle update" },
67
+ "worker_proposal": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Recorded Split Worker creation/replacement runtime card with Driver rationale" },
68
+ "worker_proposal_update": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Worker card lifecycle and manual/auto decision audit update" },
67
69
  "project_assignment_proposal": { direction: "s2c", handler: "lib/public/modules/app-message-router.js", description: "Approval card for a new owner-bound project assignment" },
68
70
  "project_assignment_status": { direction: "s2c", handler: "lib/public/modules/app-message-router.js", description: "Durable project assignment lifecycle update" },
69
71
  "project_assignment_error": { direction: "s2c", handler: "lib/public/modules/app-message-router.js", description: "Rejected or stale project assignment response" },
@@ -81,7 +83,8 @@ var schema = {
81
83
  // -----------------------------------------------------------------------
82
84
  // Chat / streaming
83
85
  // -----------------------------------------------------------------------
84
- "message": { direction: "c2s", handler: "lib/project-user-message.js", description: "Send a user chat message" },
86
+ "message": { direction: "c2s", handler: "lib/project-user-message.js", description: "Send an idempotent user chat message; message_ack confirms persistence" },
87
+ "message_ack": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Confirm that an idempotent user chat message was persisted" },
85
88
  "user_message": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Echo of the user message in the timeline" },
86
89
  "delta": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Incremental text delta from the assistant" },
87
90
  "result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Final assistant result text" },
@@ -247,9 +250,11 @@ var schema = {
247
250
  "set_update_channel": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set the update channel (stable/beta)" },
248
251
  "check_update": { direction: "c2s", handler: "lib/project-sessions.js", description: "Check if an update is available" },
249
252
  "update_now": { direction: "c2s", handler: "lib/project-sessions.js", description: "Start the update process" },
253
+ "update_snooze": { direction: "c2s", handler: "lib/project-sessions.js", description: "Snooze the current update notification for an allowlisted duration" },
254
+ "update_snoozed": { direction: "s2c", handler: "lib/public/modules/update-snooze.js", description: "Result of an update snooze, fanned out to every device of that user" },
250
255
  "update_available": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "A new version is available" },
251
256
  "up_to_date": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Server is already on the latest version" },
252
- "update_started": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Update download/install has started" },
257
+ "update_started": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Update download/install has started; the version field is included only for admin-grade recipients" },
253
258
 
254
259
  // -----------------------------------------------------------------------
255
260
  // What's New
@@ -396,12 +401,31 @@ var schema = {
396
401
  "note_written": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Agent created or updated a sticky note" },
397
402
  "note_update": { direction: "c2s", handler: "lib/project-user-message.js", description: "Update a sticky note" },
398
403
  "note_updated": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Sticky note was updated" },
399
- "note_delete": { direction: "c2s", handler: "lib/project-user-message.js", description: "Delete a sticky note" },
400
- "note_deleted": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Sticky note was deleted" },
404
+ "note_close": { direction: "c2s", handler: "lib/project-user-message.js", description: "Close a sticky note (reversible; never deletes)" },
405
+ "note_reopen": { direction: "c2s", handler: "lib/project-user-message.js", description: "Reopen a closed sticky note" },
406
+ "note_delete": { direction: "c2s", handler: "lib/project-user-message.js", description: "Retired: older clients still send this; handled as a non-destructive close" },
407
+ "note_deleted": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Retired: never sent. Closing broadcasts note_updated instead" },
401
408
  "note_list_request": { direction: "c2s", handler: "lib/project-user-message.js", description: "Request the full notes list" },
402
409
  "note_bring_front": { direction: "c2s", handler: "lib/project-user-message.js", description: "Bring a note to front (update z-order)" },
403
410
  "notes_list": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Full list of sticky notes" },
404
411
 
412
+ // -----------------------------------------------------------------------
413
+ // Project Logs
414
+ // -----------------------------------------------------------------------
415
+ "project_logs_list": { direction: "c2s", handler: "lib/project-logs.js", description: "List or search this project's logs (project scope is bound server-side)" },
416
+ "project_log_read": { direction: "c2s", handler: "lib/project-logs.js", description: "Read one project log entry by opaque ref" },
417
+ "project_log_comment": { direction: "c2s", handler: "lib/project-logs.js", description: "Append an attributed comment to a project log entry (append-only; author comes from the connection)" },
418
+ "project_log_delete": { direction: "c2s", handler: "lib/project-logs.js", description: "Delete a project log entry (project owner only; scope and identity are bound server-side)" },
419
+ "project_log_create": { direction: "c2s", handler: "lib/project-logs.js", description: "Retired. Canonical entries are written only by project agent sessions; this is always refused" },
420
+ "project_log_update": { direction: "c2s", handler: "lib/project-logs.js", description: "Retired. Canonical revisions are written only by project agent sessions; this is always refused" },
421
+ "project_logs_state": { direction: "s2c", handler: "lib/public/modules/project-logs.js", description: "Correlated project log index for a list or search request" },
422
+ "project_log_entry": { direction: "s2c", handler: "lib/public/modules/project-logs.js", description: "Correlated full project log entry" },
423
+ "project_log_commented": { direction: "s2c", handler: "lib/public/modules/project-logs.js", description: "Correlated project log entry after a comment was appended" },
424
+ "project_log_comment_reviewed": { direction: "s2c", handler: "lib/public/modules/project-logs.js", description: "A Project Driver resolved a log comment; carries identifiers and action only" },
425
+ "project_log_deleted": { direction: "s2c", handler: "lib/public/modules/project-logs.js", description: "Confirm that the project owner deleted a project log entry" },
426
+ "project_log_updated": { direction: "s2c", handler: "lib/public/modules/project-logs.js", description: "A project log entry was created, revised, or deleted (bounded ledger metadata only)" },
427
+ "project_logs_error": { direction: "s2c", handler: "lib/public/modules/project-logs.js", description: "Correlated project log request failure" },
428
+
405
429
  // -----------------------------------------------------------------------
406
430
  // Browser extension
407
431
  // -----------------------------------------------------------------------
@@ -471,6 +495,8 @@ var schema = {
471
495
  "tool_llm_op": { direction: "c2s", handler: "lib/server-tools.js", description: "Run a permission-gated vendor-neutral capsule LLM completion" },
472
496
  "tool_llm_config_get": { direction: "c2s", handler: "lib/server-tools.js", description: "Get the concrete configured provider and model used by Capsule LLM calls" },
473
497
  "tool_control_response": { direction: "c2s", handler: "lib/server-tools.js", description: "Resolve a mate tool-control request with browser runtime state or an error" },
498
+ "tool_server_control": { direction: "c2s", handler: "lib/server-tools.js", description: "Human snapshot or action against a server-runtime Capsule, through the same Logic a Mate calls" },
499
+ "tool_frame_url": { direction: "c2s", handler: "lib/server-tools.js", description: "Mint a one-time separate-origin URL for a Capsule's sandboxed rich Display frame" },
474
500
  "tools_state": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Full installed tool definitions" },
475
501
  "tool_installed": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "An installed tool changed" },
476
502
  "tool_source_state": { direction: "s2c", handler: "lib/public/modules/home-capsule-source.js", description: "Correlated authored Capsule source or read error" },
@@ -480,6 +506,10 @@ var schema = {
480
506
  "tool_llm_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Result of a capsule LLM completion" },
481
507
  "tool_llm_config_state": { direction: "s2c", handler: "lib/public/modules/tool-llm-status.js", description: "Concrete provider and model state for Capsule LLM calls" },
482
508
  "tool_control_request": { direction: "s2c", handler: "lib/public/modules/home-tools.js", description: "Ask the connected home client to snapshot or drive a sandboxed tool" },
509
+ "tool_server_state": { direction: "s2c", handler: "lib/public/modules/home-tools.js", description: "Correlated server-runtime Capsule state for the human who requested it, or the refusal that blocked it" },
510
+ "tool_server_event": { direction: "s2c", handler: "lib/public/modules/home-tools.js", description: "Ordered causal act event {seq, actor, action, previous, next} pushed to the user's Displays whenever server-runtime Capsule Logic changes state" },
511
+ "tool_frame_url_state": { direction: "s2c", handler: "lib/public/modules/home-tool-frame.js", description: "Correlated one-time rich Display frame URL, or the refusal that blocked it" },
512
+ "capsule_game_session": { direction: "s2c", handler: "lib/public/modules/home-tools.js", description: "A Capsule game session opened or advanced for this user; an explicit game start navigates the home board into it" },
483
513
  "tools_error": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Tool registry, runtime, or storage error" },
484
514
 
485
515
  // -----------------------------------------------------------------------
@@ -632,6 +662,7 @@ var schema = {
632
662
  "home_mate_session_identity": { direction: "s2c", handler: "lib/public/modules/app-message-router.js", description: "Promote an active Home session from its local reference to its durable runtime identity" },
633
663
  "home_mate_sessions_state": { direction: "s2c", handler: "lib/public/modules/home-mate-chat.js", description: "Owned visible Mate conversations with sanitized runtime identity, model, status, and timestamps" },
634
664
  "home_mate_delta": { direction: "s2c", handler: "lib/public/modules/home-mate-chat.js", description: "Selected mate's streaming assistant text delta" },
665
+ "home_mate_segment": { direction: "s2c", handler: "lib/public/modules/home-mate-chat.js", description: "A tool call ended the mate's current spoken burst; the client seals the streamed bubble and later text starts a fresh one" },
635
666
  "home_mate_done": { direction: "s2c", handler: "lib/public/modules/home-mate-chat.js", description: "Selected mate's turn finished" },
636
667
  "home_debate_proposal": { direction: "s2c", handler: "lib/public/modules/app-message-router.js", description: "Session-bound debate proposal for the active Home transcript" },
637
668
  "home_debate_proposal_resolved": { direction: "s2c", handler: "lib/public/modules/app-message-router.js", description: "Server-confirmed result for a Home debate proposal" },
@@ -424,22 +424,32 @@ async function handleQueryStart(msg) {
424
424
  function perf(label) { console.log("[PERF] sdk-worker: " + label + " +" + (Date.now() - t0) + "ms (local +" + (Date.now() - localT0) + "ms)"); }
425
425
  perf("handleQueryStart entered");
426
426
 
427
+ // Socket messages keep arriving while the SDK import awaits. Accept input
428
+ // and cancellation synchronously, before yielding to the next IPC message.
429
+ messageQueue = createMessageQueue();
430
+ abortController = new AbortController();
431
+ if (msg.prompt) messageQueue.push(msg.prompt);
432
+
427
433
  var sdk;
428
434
  try {
429
435
  perf("loading SDK");
430
436
  sdk = await getSDK();
431
437
  perf("SDK loaded");
432
438
  } catch (e) {
439
+ messageQueue.end();
440
+ messageQueue = null;
441
+ abortController = null;
442
+ sdkModule = null;
433
443
  sendToDaemon({ type: "query_error", error: "Failed to load SDK: " + (e.message || e), exitCode: null, stderr: null });
434
444
  return;
435
445
  }
436
446
 
437
- messageQueue = createMessageQueue();
438
- abortController = new AbortController();
439
-
440
- // Push the initial user message
441
- if (msg.prompt) {
442
- messageQueue.push(msg.prompt);
447
+ if (abortController.signal.aborted) {
448
+ messageQueue.end();
449
+ messageQueue = null;
450
+ abortController = null;
451
+ sendToDaemon({ type: "query_done" });
452
+ return;
443
453
  }
444
454
 
445
455
  // Build query options (callbacks are local, everything else from daemon)
@@ -20,6 +20,49 @@ var claudeUserInput = require("../claude-user-input");
20
20
  // emitted. Keep in sync with the client's generic user-dialog renderer.
21
21
  var SUPPORTED_DIALOG_KINDS = ["refusal_fallback_prompt"];
22
22
 
23
+ // Clay renders a per-task Stop control in the composer's background-tasks bar
24
+ // (public/modules/background-tasks-ui.js), wired through project-sessions.js
25
+ // `stop_task` -> adapter.stopTask -> rawQuery.stopTask (worker path: the
26
+ // stop_task message in claude-worker.js). Declaring that affordance makes an
27
+ // interrupt spare running background tasks so Stop only aborts the current
28
+ // turn. The CLI fails CLOSED on absence: without this, every interrupt would
29
+ // also kill background tasks, since a consumer with no per-task control could
30
+ // not otherwise stop a runaway one.
31
+ //
32
+ // Declared identically on the in-process and OS-user worker paths. The CLI
33
+ // takes first-attached-client-wins on multi-client sessions, so the two paths
34
+ // must not disagree, or which one connected first would decide whether Stop
35
+ // kills background work.
36
+ var PER_TASK_STOP_AFFORDANCE = true;
37
+
38
+ // --- System prompt recording (SDK `snapshot`) ---
39
+ // Records the conversation's system prompt once and replays it verbatim on
40
+ // every later request and resume, which keeps the prompt-cache prefix and
41
+ // extended-thinking context stable across CLI upgrades and flag changes.
42
+ //
43
+ // OFF for Clay's persistent sessions, deliberately. getSessionSystemPrompt
44
+ // (project.js) recomposes the append from live state every time the query is
45
+ // rebuilt - sticky notes, pair/Driver role, and the Mate Capsule catalog - and
46
+ // three call sites exist purely to force that re-render mid-conversation
47
+ // (project-capsule-catalog.js refreshSessions, project.js pair regrouping,
48
+ // sdk-bridge refreshEnvironmentRuntime). Because Clay resumes those
49
+ // conversations, a recorded prompt would pin the first render and silently
50
+ // swallow every later update until compaction.
51
+ //
52
+ // Callers whose prompt is fixed for the life of the conversation opt in with
53
+ // queryOpts.systemPromptSnapshot. A bare string prompt cannot be recorded, so
54
+ // it is promoted to the custom form the SDK requires.
55
+ function applySystemPromptSnapshot(systemPrompt, enabled) {
56
+ if (!enabled || !systemPrompt) return systemPrompt;
57
+ if (typeof systemPrompt === "string" || Array.isArray(systemPrompt)) {
58
+ return { type: "custom", prompt: systemPrompt, snapshot: true };
59
+ }
60
+ if (typeof systemPrompt === "object") {
61
+ return Object.assign({}, systemPrompt, { snapshot: true });
62
+ }
63
+ return systemPrompt;
64
+ }
65
+
23
66
  function getSharedSkillPlugins(homeDir, existingPlugins) {
24
67
  var plugins = Array.isArray(existingPlugins) ? existingPlugins.slice() : [];
25
68
  var bridgePath = skillDiscovery.ensureClaudeCodexPlugin(homeDir ? { homeDir: homeDir } : null);
@@ -45,10 +88,16 @@ function buildWorkerQueryOptions(queryOpts, claudeOpts, workerCwd) {
45
88
  if (claudeOpts.permissionMode) queryOptions.permissionMode = claudeOpts.permissionMode;
46
89
  if (claudeOpts.allowDangerouslySkipPermissions) queryOptions.allowDangerouslySkipPermissions = true;
47
90
  if (claudeOpts.settings) queryOptions.settings = claudeOpts.settings;
48
- if (queryOpts.appendSystemPrompt) queryOptions.systemPrompt = { type: "preset", preset: "claude_code", append: queryOpts.appendSystemPrompt };
91
+ if (queryOpts.appendSystemPrompt) {
92
+ queryOptions.systemPrompt = applySystemPromptSnapshot(
93
+ { type: "preset", preset: "claude_code", append: queryOpts.appendSystemPrompt },
94
+ queryOpts.systemPromptSnapshot
95
+ );
96
+ }
49
97
  queryOptions.plugins = queryOpts.skipSkills ? (claudeOpts.plugins || []) : getSharedSkillPlugins(claudeOpts.originalHome, claudeOpts.plugins);
50
98
  if (queryOpts.toolServerDescriptors) queryOptions.mcpServerDescriptors = queryOpts.toolServerDescriptors;
51
99
  if (queryOpts.onUserDialog) queryOptions.supportedDialogKinds = SUPPORTED_DIALOG_KINDS;
100
+ queryOptions.perTaskStopAffordance = PER_TASK_STOP_AFFORDANCE;
52
101
  if (queryOpts.model) queryOptions.model = queryOpts.model;
53
102
  if (queryOpts.effort) queryOptions.effort = queryOpts.effort;
54
103
  if (queryOpts.resumeSessionId) queryOptions.resume = queryOpts.resumeSessionId;
@@ -211,6 +260,18 @@ function flattenEvent(raw) {
211
260
  base.status = raw.status;
212
261
  return base;
213
262
  }
263
+ // API retry notices are transient transport state, not conversation
264
+ // errors. Preserve the structured counters so the host can bound overload
265
+ // churn without recording one red error row per SDK retry.
266
+ if (raw.subtype === "api_retry") {
267
+ base.yokeType = "api_retry";
268
+ base.attempt = raw.attempt || 0;
269
+ base.maxRetries = raw.max_retries || 0;
270
+ base.retryDelayMs = raw.retry_delay_ms || 0;
271
+ base.error = raw.error || "unknown";
272
+ base.errorStatus = raw.error_status == null ? null : raw.error_status;
273
+ return base;
274
+ }
214
275
  if (raw.subtype === "task_started") {
215
276
  base.yokeType = "task_started";
216
277
  base.parentToolId = raw.tool_use_id;
@@ -400,11 +461,21 @@ function normalizeBackgroundTasks(tasks) {
400
461
  var nativeType = task && task.task_type;
401
462
  var taskType = nativeType === "local_bash" ? "shell"
402
463
  : nativeType === "local_agent" ? "agent" : "other";
403
- return {
464
+ var normalized = {
404
465
  task_id: task && task.task_id,
405
466
  task_type: taskType,
406
467
  description: (task && task.description) || "",
468
+ // Housekeeping the CLI does not surface as user work (skip_transcript
469
+ // tasks, auto-started live-update watchers). Clay keeps these out of its
470
+ // activity indicators and lists them as a subdued secondary group.
471
+ // Normalized to a boolean so consumers never have to test for absence.
472
+ ambient: !!(task && task.ambient),
407
473
  };
474
+ // The SDK does not report a start time today. Forward one if a future
475
+ // version adds it; otherwise the session stamps first-seen time.
476
+ var startedAt = task && (task.started_at || task.startedAt || task.created_at);
477
+ if (startedAt) normalized.started_at = startedAt;
478
+ return normalized;
408
479
  });
409
480
  }
410
481
 
@@ -1449,12 +1520,17 @@ function createClaudeAdapter(opts) {
1449
1520
  if (_claudeBinaryPath) sdkOptions.pathToClaudeCodeExecutable = _claudeBinaryPath;
1450
1521
 
1451
1522
  // YOKE standard options -> SDK options
1452
- if (queryOpts.systemPrompt) sdkOptions.systemPrompt = queryOpts.systemPrompt;
1523
+ if (queryOpts.systemPrompt) {
1524
+ sdkOptions.systemPrompt = applySystemPromptSnapshot(queryOpts.systemPrompt, queryOpts.systemPromptSnapshot);
1525
+ }
1453
1526
  // Additive variant: keep the full Claude Code system prompt and append.
1454
1527
  // A plain-string systemPrompt REPLACES the whole prompt, so pair
1455
1528
  // presets and other add-ons must come through here instead.
1456
1529
  if (!queryOpts.systemPrompt && queryOpts.appendSystemPrompt) {
1457
- sdkOptions.systemPrompt = { type: "preset", preset: "claude_code", append: queryOpts.appendSystemPrompt };
1530
+ sdkOptions.systemPrompt = applySystemPromptSnapshot(
1531
+ { type: "preset", preset: "claude_code", append: queryOpts.appendSystemPrompt },
1532
+ queryOpts.systemPromptSnapshot
1533
+ );
1458
1534
  }
1459
1535
  if (queryOpts.model) sdkOptions.model = queryOpts.model;
1460
1536
  if (queryOpts.effort) sdkOptions.effort = queryOpts.effort;
@@ -1470,6 +1546,7 @@ function createClaudeAdapter(opts) {
1470
1546
  sdkOptions.onUserDialog = queryOpts.onUserDialog;
1471
1547
  sdkOptions.supportedDialogKinds = SUPPORTED_DIALOG_KINDS;
1472
1548
  }
1549
+ sdkOptions.perTaskStopAffordance = PER_TASK_STOP_AFFORDANCE;
1473
1550
  if (queryOpts.resumeSessionId) sdkOptions.resume = queryOpts.resumeSessionId;
1474
1551
  if (queryOpts.persistSession === false) sdkOptions.persistSession = false;
1475
1552