clay-server 5.2.0-beta.4 → 5.2.0-beta.5

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 (171) hide show
  1. package/lib/account-auth-socket.js +1 -0
  2. package/lib/builtin-mates.js +17 -14
  3. package/lib/canonical-clay-record-authority.js +15 -0
  4. package/lib/canonical-clay-scheduling-authority.js +19 -0
  5. package/lib/claude-hook-installer.js +28 -0
  6. package/lib/clay-mate-chat-receipts.js +26 -0
  7. package/lib/clay-mate-chat.js +87 -0
  8. package/lib/clay-mate-knowledge-input.js +26 -0
  9. package/lib/clay-mate-knowledge.js +95 -0
  10. package/lib/clay-mate-lifecycle.js +95 -0
  11. package/lib/clay-product-control-input.js +33 -0
  12. package/lib/clay-product-control-tools.js +51 -0
  13. package/lib/clay-product-control.js +237 -0
  14. package/lib/clay-project-lifecycle.js +154 -0
  15. package/lib/clay-project-record-input.js +44 -0
  16. package/lib/clay-project-records.js +83 -0
  17. package/lib/clay-scheduling-input.js +21 -0
  18. package/lib/clay-scheduling-receipts.js +26 -0
  19. package/lib/clay-scheduling.js +119 -0
  20. package/lib/clay-screen-controls.js +96 -0
  21. package/lib/clay-workspace-controls.js +83 -0
  22. package/lib/clay-workspace-input.js +17 -0
  23. package/lib/clay-workspace-receipts.js +26 -0
  24. package/lib/daemon-project-access.js +11 -0
  25. package/lib/daemon-project-lifecycle-callbacks.js +15 -0
  26. package/lib/daemon-project-lifecycle.js +262 -0
  27. package/lib/daemon-projects.js +1 -1
  28. package/lib/daemon.js +73 -221
  29. package/lib/issues-service.js +29 -15
  30. package/lib/knowledge-db-import.js +5 -4
  31. package/lib/knowledge-db-service.js +9 -6
  32. package/lib/knowledge-document-index.js +2 -1
  33. package/lib/knowledge-product-operations.js +46 -0
  34. package/lib/mate-creation-mcp-server.js +2 -0
  35. package/lib/mate-knowledge-canonical-access.js +10 -0
  36. package/lib/mate-knowledge-service.js +7 -4
  37. package/lib/mate-lifecycle-service.js +94 -0
  38. package/lib/mate-message-delivery.js +43 -0
  39. package/lib/mate-ready-creation.js +9 -1
  40. package/lib/mate-schedule-input.js +29 -0
  41. package/lib/mate-schedule-lifecycle.js +151 -0
  42. package/lib/mate-scheduling.js +103 -0
  43. package/lib/mates.js +4 -3
  44. package/lib/notes-checklist.js +50 -0
  45. package/lib/notes.js +49 -3
  46. package/lib/os-users.js +20 -5
  47. package/lib/project-admin-overview.js +51 -0
  48. package/lib/project-ask-user.js +4 -2
  49. package/lib/project-delegated-follow-up.js +2 -1
  50. package/lib/project-directory-claim.js +65 -0
  51. package/lib/project-file-operations.js +61 -0
  52. package/lib/project-filesystem.js +13 -87
  53. package/lib/project-full-access.js +23 -2
  54. package/lib/project-knowledge.js +32 -18
  55. package/lib/project-lifecycle-operations.js +98 -0
  56. package/lib/project-lifecycle-process-witness.js +32 -0
  57. package/lib/project-lifecycle-runtime.js +50 -0
  58. package/lib/project-linux-access.js +22 -0
  59. package/lib/project-logs-canonical-clay.js +27 -0
  60. package/lib/project-logs-clay-query.js +16 -0
  61. package/lib/project-logs-schema.js +4 -2
  62. package/lib/project-logs-service.js +8 -7
  63. package/lib/project-logs.js +1 -0
  64. package/lib/project-loop.js +1 -0
  65. package/lib/project-mate-chat-control.js +55 -0
  66. package/lib/project-mate-creation-proposal.js +18 -3
  67. package/lib/project-models.js +14 -31
  68. package/lib/project-notifications.js +74 -16
  69. package/lib/project-ownership.js +77 -0
  70. package/lib/project-pair-result-delivery.js +14 -3
  71. package/lib/project-pending-message-queue.js +1 -0
  72. package/lib/project-product-control.js +87 -0
  73. package/lib/project-product-session-queue.js +46 -0
  74. package/lib/project-push-access.js +28 -0
  75. package/lib/project-scheduled-message-queue.js +30 -0
  76. package/lib/project-scheduled-messages.js +63 -64
  77. package/lib/project-scheduled-task-control.js +83 -0
  78. package/lib/project-scheduled-tasks.js +13 -2
  79. package/lib/project-scheduling-access.js +10 -0
  80. package/lib/project-session-actions.js +65 -0
  81. package/lib/project-session-delete.js +8 -1
  82. package/lib/project-session-github.js +47 -12
  83. package/lib/project-session-history-actions.js +165 -0
  84. package/lib/project-session-runtime-actions.js +63 -0
  85. package/lib/project-session-vendor.js +54 -0
  86. package/lib/project-sessions.js +64 -241
  87. package/lib/project-user-message.js +30 -1
  88. package/lib/project-workspace-operations.js +126 -0
  89. package/lib/project-workspace-query.js +9 -2
  90. package/lib/project.js +95 -3
  91. package/lib/public/css/clay-tool-activity.css +51 -0
  92. package/lib/public/css/home-mate-creation.css +5 -0
  93. package/lib/public/css/overlays.css +4 -2
  94. package/lib/public/css/project-admin-overview.css +13 -0
  95. package/lib/public/css/session-folders.css +11 -0
  96. package/lib/public/css/session-github.css +3 -0
  97. package/lib/public/css/sticky-notes.css +61 -1
  98. package/lib/public/index.html +1 -1
  99. package/lib/public/modules/admin-settings-projects.js +6 -2
  100. package/lib/public/modules/app-connection.js +8 -1
  101. package/lib/public/modules/app-message-router.js +1 -1
  102. package/lib/public/modules/app-messages.js +18 -3
  103. package/lib/public/modules/app-projects.js +5 -0
  104. package/lib/public/modules/clay-owner-navigation.js +27 -0
  105. package/lib/public/modules/clay-tool-activity.js +140 -0
  106. package/lib/public/modules/clay-tool-presentation.js +338 -0
  107. package/lib/public/modules/command-palette.js +1 -1
  108. package/lib/public/modules/default-ai.js +11 -3
  109. package/lib/public/modules/home-debate-planning.js +5 -5
  110. package/lib/public/modules/home-mate-chat.js +8 -6
  111. package/lib/public/modules/home-mate-creation.js +73 -1
  112. package/lib/public/modules/personal-settings-dialog.js +7 -1
  113. package/lib/public/modules/project-access-recovery.js +17 -0
  114. package/lib/public/modules/project-admin-overview.js +21 -0
  115. package/lib/public/modules/session-folder-layout.js +1 -1
  116. package/lib/public/modules/session-folder-view-menu.js +9 -0
  117. package/lib/public/modules/session-github.js +65 -8
  118. package/lib/public/modules/sidebar-session-folders-ui.js +1 -1
  119. package/lib/public/modules/sticky-note-markdown.js +72 -6
  120. package/lib/public/modules/sticky-notes-actions.js +30 -0
  121. package/lib/public/modules/sticky-notes-browser.js +9 -2
  122. package/lib/public/modules/sticky-notes-card.js +23 -7
  123. package/lib/public/modules/sticky-notes-checklist.js +71 -0
  124. package/lib/public/modules/sticky-notes-editor.js +108 -44
  125. package/lib/public/modules/sticky-notes-shared.js +50 -2
  126. package/lib/public/modules/sticky-notes-state.js +66 -0
  127. package/lib/public/modules/sticky-notes.js +34 -51
  128. package/lib/public/modules/thinking-lifecycle.js +33 -3
  129. package/lib/public/modules/thinking-view.js +23 -1
  130. package/lib/public/modules/tools.js +34 -6
  131. package/lib/public/modules/user-settings-appearance.js +10 -3
  132. package/lib/public/modules/user-settings-editors.js +1 -1
  133. package/lib/public/modules/user-settings-layout.js +11 -2
  134. package/lib/public/modules/user-settings-preferences.js +6 -2
  135. package/lib/public/modules/user-settings-profile.js +12 -2
  136. package/lib/public/modules/user-settings.js +3 -2
  137. package/lib/public/style.css +2 -0
  138. package/lib/scheduled-task-records.js +3 -1
  139. package/lib/scheduler.js +2 -0
  140. package/lib/sdk-bridge.js +220 -21
  141. package/lib/sdk-message-processor.js +2 -0
  142. package/lib/sdk-response-recovery.js +155 -0
  143. package/lib/server-admin.js +63 -16
  144. package/lib/server-dm.js +1 -1
  145. package/lib/server-global-ws.js +10 -20
  146. package/lib/server-home-chat-events.js +7 -3
  147. package/lib/server-home-chat.js +4 -16
  148. package/lib/server-home-mate-creation.js +1 -0
  149. package/lib/server-home-mcp-consent.js +3 -3
  150. package/lib/server-mates.js +25 -67
  151. package/lib/server-owner-ui.js +77 -0
  152. package/lib/server-path-access.js +38 -0
  153. package/lib/server-project-removal.js +22 -0
  154. package/lib/server.js +99 -29
  155. package/lib/session-config-transition.js +45 -0
  156. package/lib/session-folders.js +7 -2
  157. package/lib/session-github.js +9 -2
  158. package/lib/session-visibility.js +5 -2
  159. package/lib/sessions.js +10 -0
  160. package/lib/terminal-manager.js +52 -0
  161. package/lib/users-permissions.js +2 -8
  162. package/lib/users.js +4 -0
  163. package/lib/workspace-query-access.js +4 -1
  164. package/lib/ws-schema.js +8 -2
  165. package/lib/yoke/adapters/claude.js +47 -8
  166. package/lib/yoke/adapters/codex.js +52 -111
  167. package/lib/yoke/claude-worker-startup.js +28 -0
  168. package/lib/yoke/codex-idle-recovery.js +81 -1
  169. package/lib/yoke/codex-recovery-admission.js +164 -0
  170. package/lib/yoke/session-tool-transport.js +3 -0
  171. package/package.json +1 -1
package/lib/daemon.js CHANGED
@@ -67,6 +67,8 @@ try {
67
67
  console.error("[daemon] ensureMultiUser threw (continuing):", e && e.message);
68
68
  }
69
69
 
70
+ config = require("./project-ownership").repair(config, usersModule.getAllUsers(), saveConfig, osUsersMod.resolveOsUserInfo);
71
+
70
72
  if (projectLogContext.initializeProjectKnowledge(config)) {
71
73
  saveConfig(config);
72
74
  console.log("[daemon] Assigned stable knowledge identities to existing projects");
@@ -187,7 +189,14 @@ function registerProjectWorktrees(project, requestUser) {
187
189
  }
188
190
  }
189
191
 
192
+ var projectLifecycle = require("./daemon-project-lifecycle").attachProjectLifecycle({
193
+ getConfig: function () { return config; }, setConfig: function (next) { config = next; }, saveConfig: saveConfig,
194
+ getRelay: function () { return relay; }, resolveIdentity: osUsersMod.resolveOsUserInfo,
195
+ findUser: usersModule.findUserById, registerWorktrees: registerProjectWorktrees,
196
+ });
197
+ var lifecycleCallbacks = require("./daemon-project-lifecycle-callbacks").attachLifecycleCallbacks({ service: projectLifecycle });
190
198
  var relay = createServer({
199
+ projectLifecycle: projectLifecycle,
191
200
  tlsOptions: tlsOptions,
192
201
  caPath: caRoot,
193
202
  builtinCert: config.builtinCert || false,
@@ -198,122 +207,11 @@ var relay = createServer({
198
207
  osUsers: config.osUsers || false,
199
208
  lanHost: lanIp ? lanIp + ":" + config.port : null,
200
209
  getRemovedProjects: function (userId) { return getFilteredRemovedProjects(config, userId); },
201
- onAddProject: function (absPath, wsUser) {
202
- // Check if already registered
203
- for (var j = 0; j < config.projects.length; j++) {
204
- if (config.projects[j].path === absPath) {
205
- return { ok: true, slug: config.projects[j].slug, existing: true };
206
- }
207
- }
208
- var slugs = config.projects.map(function (p) { return p.slug; });
209
- var slug = generateSlug(absPath, slugs);
210
- var projectEntry = { path: absPath, slug: slug, addedAt: Date.now(), visibility: "private" };
211
- projectEntry.projectKnowledgeId = rememberedProjectKnowledgeId(absPath);
212
- // The user who adds a project always becomes the owner
213
- if (wsUser && wsUser.id) {
214
- projectEntry.ownerId = wsUser.id;
215
- }
216
- config.projects.push(projectEntry);
217
- relay.addProject(absPath, slug, null, null, wsUser ? wsUser.id : null, null, projectRuntimeOptions(projectEntry));
218
- // Remove from removedProjects if present
219
- if (config.removedProjects) {
220
- config.removedProjects = config.removedProjects.filter(function (rp) { return rp.path !== absPath; });
221
- }
222
- saveConfig(config);
223
- try { syncClayrc(config.projects); } catch (e) {}
224
- console.log("[daemon] Added project (web):", slug, "→", absPath);
225
- // OS users mode: grant ACL to project owner
226
- if (config.osUsers) {
227
- var newProj = config.projects[config.projects.length - 1];
228
- if (newProj.ownerId) {
229
- var ownerUser = usersModule.findUserById(newProj.ownerId);
230
- if (ownerUser && ownerUser.linuxUser) {
231
- grantProjectAccess(absPath, ownerUser.linuxUser);
232
- }
233
- }
234
- }
235
- // Discover and register worktrees for the new project
236
- registerProjectWorktrees(projectEntry, wsUser);
237
- // Broadcast updated project list to all clients
238
- relay.broadcastAll({
239
- type: "projects_updated",
240
- projects: relay.getProjects(),
241
- projectCount: config.projects.length,
242
- });
243
- return { ok: true, slug: slug };
244
- },
245
- onCreateProject: function (projectName, wsUser) {
246
- console.log("[daemon] onCreateProject wsUser:", JSON.stringify(wsUser ? { id: wsUser.id, role: wsUser.role, username: wsUser.username, linuxUser: wsUser.linuxUser } : null));
247
- var os = require("os");
248
- var execFileSync = require("child_process").execFileSync;
249
- var baseDir;
250
- if (config.osUsers) {
251
- baseDir = "/var/clay/projects";
252
- } else {
253
- baseDir = config.projectsDir || path.join(REAL_HOME, "clay-projects");
254
- }
255
- try { fs.mkdirSync(baseDir, { recursive: true }); } catch (e) {}
256
- // Generate slug and deduplicate
257
- var slugs = config.projects.map(function (p) { return p.slug; });
258
- var slug = generateSlug(path.join(baseDir, projectName), slugs);
259
- var targetDir = path.join(baseDir, slug);
260
- try {
261
- fs.mkdirSync(targetDir, { recursive: true });
262
- // Run git init
263
- if (config.osUsers && wsUser) {
264
- var linuxUser = wsUser.linuxUser;
265
- if (linuxUser) {
266
- var uidGid = null;
267
- try {
268
- uidGid = {
269
- uid: parseInt(execFileSync("id", ["-u", linuxUser], { encoding: "utf8", stdio: "pipe" }).trim(), 10),
270
- gid: parseInt(execFileSync("id", ["-g", linuxUser], { encoding: "utf8", stdio: "pipe" }).trim(), 10),
271
- };
272
- } catch (e) {}
273
- if (uidGid) {
274
- fs.chmodSync(targetDir, 0o700);
275
- execFileSync("chown", ["-R", linuxUser + ":" + linuxUser, targetDir]);
276
- var gitInit = osUsersMod.wrapSpawnAsUser("git", ["init"], { cwd: targetDir, uid: uidGid.uid, gid: uidGid.gid, env: { PATH: "/usr/local/bin:/usr/bin:/bin" } });
277
- execFileSync(gitInit.command, gitInit.args, gitInit.options);
278
- } else {
279
- execFileSync("git", ["init"], { cwd: targetDir });
280
- }
281
- } else {
282
- execFileSync("git", ["init"], { cwd: targetDir });
283
- }
284
- } else {
285
- execFileSync("git", ["init"], { cwd: targetDir });
286
- }
287
- } catch (e) {
288
- try { fs.rmSync(targetDir, { recursive: true, force: true }); } catch (ce) {}
289
- return { ok: false, error: "Failed to create project: " + e.message };
290
- }
291
- // Register project - creator always becomes owner, default private
292
- var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now(), visibility: "private" };
293
- projectEntry.projectKnowledgeId = projectLogContext.createProjectKnowledgeId();
294
- if (wsUser && wsUser.id) {
295
- projectEntry.ownerId = wsUser.id;
296
- }
297
- relay.addProject(targetDir, slug, null, null, wsUser ? wsUser.id : null, null, projectRuntimeOptions(projectEntry));
298
- config.projects.push(projectEntry);
299
- saveConfig(config);
300
- try { syncClayrc(config.projects); } catch (e) {}
301
- console.log("[daemon] Created project:", slug, "→", targetDir, "entry:", JSON.stringify({ ownerId: projectEntry.ownerId, visibility: projectEntry.visibility }));
302
- // OS users mode: grant ACL
303
- if (config.osUsers && wsUser && wsUser.linuxUser) {
304
- console.log("[daemon] Granting ACL:", targetDir, "→", wsUser.linuxUser);
305
- grantProjectAccess(targetDir, wsUser.linuxUser);
306
- } else if (config.osUsers) {
307
- console.log("[daemon] Skipping ACL grant: osUsers=true but linuxUser=", wsUser && wsUser.linuxUser);
308
- }
309
- relay.broadcastAll({
310
- type: "projects_updated",
311
- projects: relay.getProjects(),
312
- projectCount: config.projects.length,
313
- });
314
- return { ok: true, slug: slug };
315
- },
210
+ onAddProject: lifecycleCallbacks.onAddProject,
211
+ onCreateProject: lifecycleCallbacks.onCreateProject,
316
212
  onCloneProject: function (cloneUrl, wsUser, callback) {
213
+ wsUser = wsUser && usersModule.findUserById(wsUser.id);
214
+ if (usersModule.isMultiUser() && !wsUser) { callback({ ok: false, error: "A valid account is required." }); return; }
317
215
  var os = require("os");
318
216
  var spawn = require("child_process").spawn;
319
217
  var execFileSync = require("child_process").execFileSync;
@@ -323,12 +221,15 @@ var relay = createServer({
323
221
  } else {
324
222
  baseDir = config.projectsDir || path.join(REAL_HOME, "clay-projects");
325
223
  }
326
- try { fs.mkdirSync(baseDir, { recursive: true }); } catch (e) {}
327
224
  // Derive slug from repo URL
328
225
  var repoName = cloneUrl.replace(/\.git$/, "").split("/").pop() || "project";
329
226
  var slugs = config.projects.map(function (p) { return p.slug; });
330
227
  var slug = generateSlug(path.join(baseDir, repoName), slugs);
331
- var targetDir = path.join(baseDir, slug);
228
+ var targetDir;
229
+ var directoryClaim;
230
+ var settled = false;
231
+ function finish(result) { if (settled) return; settled = true; callback(result); }
232
+ function cleanup() { if (directoryClaim) { try { directoryClaim.cleanup(); } catch (error) {} } }
332
233
  // Clone as user to use their git credentials
333
234
  var spawnOpts = { cwd: baseDir };
334
235
  if (config.osUsers && wsUser && wsUser.linuxUser) {
@@ -341,22 +242,23 @@ var relay = createServer({
341
242
  });
342
243
  } catch (e) {}
343
244
  }
344
- // Pre-create target directory as root and chown to user
345
- if (fs.existsSync(targetDir)) {
346
- callback({ ok: false, error: "Target directory already exists: " + targetDir });
245
+ try {
246
+ directoryClaim = require("./project-directory-claim").claim({ baseDir: baseDir, slug: slug, projects: config.projects, userId: wsUser && wsUser.id });
247
+ targetDir = directoryClaim.path;
248
+ directoryClaim.assertOwned();
249
+ if (config.osUsers && wsUser && wsUser.linuxUser && spawnOpts.uid != null) fs.chownSync(targetDir, spawnOpts.uid, spawnOpts.gid);
250
+ directoryClaim.assertOwned();
251
+ } catch (error) {
252
+ cleanup();
253
+ finish({ ok: false, error: "Failed to prepare project directory: " + error.message });
347
254
  return;
348
255
  }
349
- if (config.osUsers && wsUser && wsUser.linuxUser && spawnOpts.uid != null) {
350
- try {
351
- fs.mkdirSync(targetDir, { mode: 0o700 });
352
- fs.chownSync(targetDir, spawnOpts.uid, spawnOpts.gid);
353
- } catch (e) {
354
- callback({ ok: false, error: "Failed to prepare project directory: " + e.message });
355
- return;
356
- }
357
- }
358
- var cloneSpawn = osUsersMod.wrapSpawnAsUser("git", ["clone", cloneUrl, targetDir], spawnOpts);
359
- var proc = spawn(cloneSpawn.command, cloneSpawn.args, cloneSpawn.options);
256
+ var proc;
257
+ try {
258
+ directoryClaim.assertOwned();
259
+ var cloneSpawn = osUsersMod.wrapSpawnAsUser("git", ["clone", cloneUrl, targetDir], spawnOpts);
260
+ proc = spawn(cloneSpawn.command, cloneSpawn.args, cloneSpawn.options);
261
+ } catch (error) { cleanup(); finish({ ok: false, error: "Failed to start git clone: " + error.message }); return; }
360
262
  var stderrBuf = "";
361
263
  proc.stderr.on("data", function (chunk) { stderrBuf += chunk.toString(); });
362
264
  // 5 minute timeout
@@ -365,19 +267,23 @@ var relay = createServer({
365
267
  }, 5 * 60 * 1000);
366
268
  proc.on("close", function (code) {
367
269
  clearTimeout(cloneTimeout);
270
+ if (settled) return;
271
+ try { directoryClaim.assertOwned(); } catch (error) { finish({ ok: false, error: error.message }); return; }
368
272
  if (code !== 0) {
369
- try { fs.rmSync(targetDir, { recursive: true, force: true }); } catch (ce) {}
273
+ cleanup();
370
274
  var errMsg = stderrBuf.trim().split("\n").pop() || "Clone failed (exit code " + code + ")";
371
- callback({ ok: false, error: errMsg });
275
+ finish({ ok: false, error: errMsg });
372
276
  return;
373
277
  }
374
278
  // chown and restrict permissions if osUsers
375
279
  if (config.osUsers && wsUser && wsUser.linuxUser) {
376
280
  try {
281
+ directoryClaim.assertOwned();
377
282
  fs.chmodSync(targetDir, 0o700);
378
283
  execFileSync("chown", ["-R", wsUser.linuxUser + ":" + wsUser.linuxUser, targetDir]);
379
284
  } catch (e) {}
380
285
  }
286
+ if (usersModule.isMultiUser() && (!wsUser || !usersModule.findUserById(wsUser.id))) { finish({ ok: false, error: "The creating account is no longer available." }); return; }
381
287
  // Register project - creator always becomes owner
382
288
  // Creator always becomes owner, default private
383
289
  var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now(), visibility: "private" };
@@ -385,9 +291,11 @@ var relay = createServer({
385
291
  if (wsUser && wsUser.id) {
386
292
  projectEntry.ownerId = wsUser.id;
387
293
  }
388
- relay.addProject(targetDir, slug, null, null, wsUser ? wsUser.id : null, null, projectRuntimeOptions(projectEntry));
389
- config.projects.push(projectEntry);
390
- saveConfig(config);
294
+ try {
295
+ directoryClaim.assertOwned();
296
+ config = require("./project-ownership").register(config, projectEntry, usersModule.getAllUsers(), saveConfig, osUsersMod.resolveOsUserInfo);
297
+ } catch (error) { finish({ ok: false, error: "Failed to register project: " + error.message }); return; }
298
+ relay.addProject(targetDir, slug, null, null, projectEntry.ownerId || null, null, projectRuntimeOptions(projectEntry));
391
299
  try { syncClayrc(config.projects); } catch (e) {}
392
300
  console.log("[daemon] Cloned project:", slug, "→", targetDir);
393
301
  if (config.osUsers && wsUser && wsUser.linuxUser) {
@@ -398,12 +306,13 @@ var relay = createServer({
398
306
  projects: relay.getProjects(),
399
307
  projectCount: config.projects.length,
400
308
  });
401
- callback({ ok: true, slug: slug });
309
+ finish({ ok: true, slug: slug });
402
310
  });
403
311
  proc.on("error", function (err) {
404
312
  clearTimeout(cloneTimeout);
405
- try { fs.rmSync(targetDir, { recursive: true, force: true }); } catch (ce) {}
406
- callback({ ok: false, error: "Failed to start git clone: " + err.message });
313
+ if (settled) return;
314
+ cleanup();
315
+ finish({ ok: false, error: "Failed to start git clone: " + err.message });
407
316
  });
408
317
  },
409
318
  onRemoveProject: function (slug, userId) {
@@ -444,40 +353,7 @@ var relay = createServer({
444
353
  });
445
354
  return { ok: true };
446
355
  }
447
- var found = null;
448
- for (var j = 0; j < config.projects.length; j++) {
449
- if (config.projects[j].slug === slug) { found = config.projects[j]; break; }
450
- }
451
- if (!found) return { ok: false, error: "Project not found" };
452
- // Cascade remove worktrees belonging to this parent
453
- cleanupWorktreesForParent(relay, slug);
454
- // Save to removedProjects for re-add functionality
455
- if (!config.removedProjects) config.removedProjects = [];
456
- config.removedProjects.push({
457
- path: found.path,
458
- title: found.title || null,
459
- icon: found.icon || null,
460
- userId: userId || null,
461
- projectKnowledgeId: found.projectKnowledgeId || null,
462
- removedAt: Date.now(),
463
- });
464
- // Cap at 20 entries (oldest first)
465
- if (config.removedProjects.length > 20) {
466
- config.removedProjects = config.removedProjects.slice(config.removedProjects.length - 20);
467
- }
468
- relay.removeProject(slug);
469
- config.projects = config.projects.filter(function (p) { return p.slug !== slug; });
470
- saveConfig(config);
471
- // Remove from .clayrc so it doesn't appear in restore prompt
472
- if (found.path) { try { removeFromClayrc(found.path); } catch (e) {} }
473
- try { syncClayrc(config.projects); } catch (e) {}
474
- console.log("[daemon] Removed project (web):", slug);
475
- relay.broadcastAll({
476
- type: "projects_updated",
477
- projects: relay.getProjects(),
478
- projectCount: config.projects.length,
479
- });
480
- return { ok: true };
356
+ return lifecycleCallbacks.onRemoveProject(slug, userId);
481
357
  },
482
358
  onReorderProjects: function (slugs) {
483
359
  // Build a slug->project map from current projects
@@ -511,25 +387,8 @@ var relay = createServer({
511
387
  return { ok: true };
512
388
  },
513
389
  onSetProjectTitle: function (slug, newTitle) {
514
- relay.setProjectTitle(slug, newTitle);
515
- for (var ti = 0; ti < config.projects.length; ti++) {
516
- if (config.projects[ti].slug === slug) {
517
- if (newTitle) {
518
- config.projects[ti].title = newTitle;
519
- } else {
520
- delete config.projects[ti].title;
521
- }
522
- break;
523
- }
524
- }
525
- saveConfig(config);
526
- try { syncClayrc(config.projects); } catch (e) {}
527
- relay.broadcastAll({
528
- type: "projects_updated",
529
- projects: relay.getProjects(),
530
- projectCount: config.projects.length,
531
- });
532
- return { ok: true };
390
+ if (isWorktreeSlug(slug)) { relay.setProjectTitle(slug, newTitle); return { ok: true }; }
391
+ return lifecycleCallbacks.onSetProjectTitle(slug, newTitle);
533
392
  },
534
393
  onSetProjectIcon: function (slug, newIcon) {
535
394
  relay.setProjectIcon(slug, newIcon);
@@ -553,30 +412,28 @@ var relay = createServer({
553
412
  return { ok: true };
554
413
  },
555
414
  onSetProjectSessionVisibilityDefault: function (slug, visibility) {
556
- var result = daemonProjectAccess.setSessionVisibilityDefault(config, slug, visibility);
557
- if (result.error) return { ok: false, error: result.error };
558
- saveConfig(config);
559
- relay.broadcastAll({ type: "projects_updated", projects: relay.getProjects(), projectCount: config.projects.length });
560
- return result;
415
+ return projectLifecycle.update(slug, "session_visibility", visibility);
561
416
  },
562
417
  onProjectOwnerChanged: function (slug, ownerId) {
418
+ if (!config.osUsers) return projectLifecycle.update(slug, "owner", ownerId);
563
419
  if (slug.indexOf("mate-") === 0) return {error:"Mate ownership cannot be transferred."};
420
+ if (!ownerId || !usersModule.findUserById(ownerId)) return { error: "A valid owner is required." };
564
421
  console.log("[daemon] onProjectOwnerChanged:", slug, "→", ownerId);
422
+ var previousConfig = JSON.parse(JSON.stringify(config));
565
423
  var oldOwnerId = null;
566
424
  var projectIdx = -1;
567
425
  for (var oi = 0; oi < config.projects.length; oi++) {
568
426
  if (config.projects[oi].slug === slug) {
569
427
  oldOwnerId = config.projects[oi].ownerId || null;
570
428
  projectIdx = oi;
571
- if (ownerId) {
572
- config.projects[oi].ownerId = ownerId;
573
- } else {
574
- delete config.projects[oi].ownerId;
575
- }
429
+ config.projects[oi].ownerId = ownerId;
576
430
  break;
577
431
  }
578
432
  }
579
- saveConfig(config);
433
+ try { saveConfig(config); } catch (error) { config = previousConfig; throw error; }
434
+ relay.forEachProject(function (project, projectSlug) {
435
+ if (projectSlug === slug || daemonProjectAccess.parentSlugFor(projectSlug) === slug) project.setProjectOwner(ownerId);
436
+ });
580
437
  // OS users mode: revoke old owner ACL, grant new owner ACL
581
438
  if (config.osUsers && projectIdx >= 0) {
582
439
  var projectPath = config.projects[projectIdx].path;
@@ -615,18 +472,7 @@ var relay = createServer({
615
472
  return { envrc: "" };
616
473
  },
617
474
  onSetProjectEnv: function (slug, envrc) {
618
- for (var ei = 0; ei < config.projects.length; ei++) {
619
- if (config.projects[ei].slug === slug) {
620
- if (envrc) {
621
- config.projects[ei].envrc = envrc;
622
- } else {
623
- delete config.projects[ei].envrc;
624
- }
625
- saveConfig(config);
626
- return { ok: true };
627
- }
628
- }
629
- return { ok: false, error: "Project not found" };
475
+ return lifecycleCallbacks.onSetProjectEnv(slug, envrc);
630
476
  },
631
477
  onGetSharedEnv: function () {
632
478
  return { envrc: config.sharedEnv || "" };
@@ -927,6 +773,7 @@ var relay = createServer({
927
773
  spawnAndRestart();
928
774
  },
929
775
  onSetProjectVisibility: function (slug, visibility) {
776
+ if (!config.osUsers) return projectLifecycle.update(slug, "visibility", visibility);
930
777
  if (slug.indexOf("mate-") === 0) return {error:"Mates cannot be shared."};
931
778
  for (var i = 0; i < config.projects.length; i++) {
932
779
  if (config.projects[i].slug === slug) {
@@ -962,6 +809,7 @@ var relay = createServer({
962
809
  return { error: "Project not found" };
963
810
  },
964
811
  onSetProjectAllowedUsers: function (slug, allowedUsers) {
812
+ if (!config.osUsers && !daemonProjectAccess.parentSlugFor(slug)) return projectLifecycle.update(slug, "allowed_users", allowedUsers);
965
813
  var accessBefore = daemonProjectAccess.getProjectAccess(config, slug, config.osUsers);
966
814
  var result = daemonProjectAccess.setAllowedUsers(config, slug, allowedUsers);
967
815
  if (result.error) return result;
@@ -1131,11 +979,11 @@ var ipc = createIPCServer(socketPath(), function (msg) {
1131
979
  }
1132
980
  var slugs = config.projects.map(function (p) { return p.slug; });
1133
981
  var slug = generateSlug(absPath, slugs);
1134
- var projectEntry = { path: absPath, slug: slug, addedAt: Date.now(), visibility: "private" };
982
+ var projectEntry = { path: absPath, slug: slug, addedAt: Date.now(), visibility: "private", ownershipRecovery: "required" };
1135
983
  projectEntry.projectKnowledgeId = rememberedProjectKnowledgeId(absPath);
1136
- relay.addProject(absPath, slug, null, null, null, null, projectRuntimeOptions(projectEntry));
1137
- config.projects.push(projectEntry);
1138
- saveConfig(config);
984
+ config = require("./project-ownership").register(config, projectEntry, usersModule.getAllUsers(), saveConfig, osUsersMod.resolveOsUserInfo);
985
+ var registeredProject = config.projects[config.projects.length - 1];
986
+ relay.addProject(absPath, slug, null, null, registeredProject.ownerId || null, null, projectRuntimeOptions(registeredProject));
1139
987
  try { syncClayrc(config.projects); } catch (e) {}
1140
988
  console.log("[daemon] Added project:", slug, "→", absPath);
1141
989
  // Discover and register worktrees for the new project
@@ -1367,6 +1215,10 @@ var ipc = createIPCServer(socketPath(), function (msg) {
1367
1215
  var projects = config.projects || [];
1368
1216
  for (var i = 0; i < projects.length; i++) {
1369
1217
  var p = projects[i];
1218
+ if (!projectLifecycle.canRestore(p)) {
1219
+ console.log("[daemon] Project removal needs recovery; runtime remains stopped:", p.slug);
1220
+ continue;
1221
+ }
1370
1222
  if (fs.existsSync(p.path)) {
1371
1223
  console.log("[daemon] Adding project:", p.slug, "→", p.path);
1372
1224
  relay.addProject(p.path, p.slug, p.title, p.icon, p.ownerId, null, projectRuntimeOptions(p));
@@ -1413,7 +1265,7 @@ if (usersModule.isMultiUser()) {
1413
1265
  var mateDir = mates.getMateDir(mateCtx, m.id);
1414
1266
  var mateSlug = "mate-" + m.id;
1415
1267
  var mateName = (m.profile && m.profile.displayName) || m.name || "New Mate";
1416
- if (fs.existsSync(mateDir)) {
1268
+ if (!m.lifecycleDeletion && fs.existsSync(mateDir)) {
1417
1269
  var mateDef = m.builtinKey ? require("./builtin-mates").getBuiltinByKey(m.builtinKey) : null;
1418
1270
  var isHost = !!(mateDef && mateDef.hostAgent);
1419
1271
  console.log("[daemon] Adding mate project:", mateSlug + (isHost ? " (host agent)" : ""));
@@ -1430,7 +1282,7 @@ if (usersModule.isMultiUser()) {
1430
1282
  var mateDir = mates.getMateDir(mateCtx, m.id);
1431
1283
  var mateSlug = "mate-" + m.id;
1432
1284
  var mateName = (m.profile && m.profile.displayName) || m.name || "New Mate";
1433
- if (fs.existsSync(mateDir)) {
1285
+ if (!m.lifecycleDeletion && fs.existsSync(mateDir)) {
1434
1286
  var mateDef2 = m.builtinKey ? require("./builtin-mates").getBuiltinByKey(m.builtinKey) : null;
1435
1287
  var isHost2 = !!(mateDef2 && mateDef2.hostAgent);
1436
1288
  console.log("[daemon] Adding mate project:", mateSlug + (isHost2 ? " (host agent)" : ""));
@@ -129,25 +129,35 @@ function attachIssuesService(ctx) {
129
129
  principal: principal, worktreeOnly: authorization.worktreeOnly };
130
130
  }
131
131
 
132
- function bind(source, isSession) {
132
+ function bind(source, isSession, canonicalClay) {
133
133
  if (!source || typeof source.projectSlug !== "string") return null;
134
134
  var initialSlug = source.projectSlug;
135
135
  var initialProject = projectFor(initialSlug);
136
136
  var initialSession = isSession ? source.session : null;
137
+ var clayInitial;
138
+ if (canonicalClay) { try { clayInitial = require("./canonical-clay-record-authority").resolve(source.authority, initialSlug); } catch (error) { return null; } }
137
139
  if (!initialProject || (isSession && !initialSession)) return null;
138
140
  if (initialSession) provenance.ensureOrigin(initialSession);
139
- var initialUserId = initialSession ? initialSession.ownerId || null : source.userId || null;
141
+ var initialUserId = initialSession ? initialSession.ownerId || null : canonicalClay ? clayInitial.principal.userId || clayInitial.principal.singleUserOwnerId : source.userId || null;
140
142
  var initialOrigin = initialSession ? initialSession.sessionOriginId || null : null;
141
143
  var initial = bindingState(initialProject, initialSession, initialUserId, initialOrigin);
142
144
  if (!initial) return null;
143
145
 
144
146
  function current() {
145
147
  if (projectFor(initialSlug) !== initialProject) throw new Error("This Issues binding is no longer valid.");
148
+ if (canonicalClay) {
149
+ var clayLive = require("./canonical-clay-record-authority").resolve(source.authority, initialSlug);
150
+ if (clayLive.project !== initialProject || clayLive.status.path !== clayInitial.status.path || clayLive.status.projectKnowledgeId !== clayInitial.status.projectKnowledgeId || clayLive.status.changeSetId !== clayInitial.status.changeSetId) throw new Error("Selected Issues context changed.");
151
+ }
146
152
  var live = bindingState(initialProject, initialSession, initialUserId, initialOrigin);
147
153
  if (!live) throw new Error("This Issues binding is no longer valid.");
148
154
  return live;
149
155
  }
150
156
 
157
+ function expected(entry, args) { if (canonicalClay && (!Number.isInteger(args.expectedRevision) || entry.revision !== args.expectedRevision)) throw new Error("Issue revision conflict."); }
158
+
159
+ function actor(state) { return canonicalClay ? require("./canonical-clay-record-authority").resolve(source.authority, initialSlug).actor : author(state.user, state.session ? "session" : "user", state.session); }
160
+
151
161
  function visibleIssue(store, ref, state) {
152
162
  var result = store.read(ref);
153
163
  if (state.worktreeOnly && (!result.context || result.context.changeSetId !== state.status.changeSetId)) throw new Error("Issue not found.");
@@ -174,7 +184,7 @@ function attachIssuesService(ctx) {
174
184
  path: state.status.path, knowledgeId: governing.projectKnowledgeId,
175
185
  changeSetId: state.status.changeSetId || null, worktreeOnly: state.worktreeOnly,
176
186
  recordContext: clone(issueContext(state.status)),
177
- actor: clone(author(state.user, state.session ? "session" : "user", state.session)),
187
+ actor: clone(actor(state)),
178
188
  osIdentity: needsIdentity ? osIdentity(state) : null };
179
189
  }
180
190
 
@@ -184,7 +194,7 @@ function attachIssuesService(ctx) {
184
194
  var liveIdentity = needsIdentity ? osIdentity(fresh) : null;
185
195
  if (fresh.project !== before.project || fresh.session !== before.session || (fresh.user.id || null) !== before.userId ||
186
196
  fresh.status.path !== before.path || !governing || governing.projectKnowledgeId !== before.knowledgeId ||
187
- (fresh.status.changeSetId || null) !== before.changeSetId || fresh.worktreeOnly !== before.worktreeOnly ||
197
+ (fresh.status.changeSetId || null) !== before.changeSetId || fresh.worktreeOnly !== before.worktreeOnly || JSON.stringify(issueContext(fresh.status)) !== JSON.stringify(before.recordContext) ||
188
198
  JSON.stringify(liveIdentity) !== JSON.stringify(before.osIdentity)) throw new Error("Issue authorization changed during the operation.");
189
199
  return fresh;
190
200
  }
@@ -200,7 +210,7 @@ function attachIssuesService(ctx) {
200
210
  var github = require('./issues-github').attachIssueGithub({
201
211
  locks: githubLocks, assessments: githubAssessments, run: context.runGithub, analyze: source.analyzeGithub || context.analyzeGithub,
202
212
  begin: function () {
203
- if (isSession) throw new Error('GitHub filing requires an explicit action in the Issues panel.');
213
+ if (isSession || canonicalClay) throw new Error('GitHub filing requires an explicit action in the Issues panel.');
204
214
  return operation(current(), true);
205
215
  },
206
216
  read: function (before, ref) {
@@ -217,6 +227,7 @@ function attachIssuesService(ctx) {
217
227
  return {
218
228
  searchGithubIssue: github.search, linkGithubIssue: github.link, createGithubIssue: github.create,
219
229
  linkWorkSession: function (args, session) {
230
+ if (canonicalClay) throw new Error("Use reviewed project assignment or follow-up for Driver execution.");
220
231
  var state = current();
221
232
  var manager = state.project.getSessionManager();
222
233
  if (!args || !Number.isInteger(args.expectedRevision)) throw new Error("expectedRevision is required.");
@@ -232,9 +243,10 @@ function attachIssuesService(ctx) {
232
243
  if (links.some(function (link) { return link.sessionId === session.sessionOriginId; })) return entry;
233
244
  var link = { sessionId: session.sessionOriginId, role: "driver", issueRevision: args.expectedRevision };
234
245
  if (args.requestId !== undefined) link.requestId = schema.text(args.requestId, "work request id", 200, true);
235
- return target.update(args.ref, { linkedWorkSessions: links.concat([link]), status: "in_progress" }, args.expectedRevision, author(state.user, "user", null));
246
+ return target.update(args.ref, { linkedWorkSessions: links.concat([link]), status: "in_progress" }, args.expectedRevision, canonicalClay ? actor(state) : author(state.user, "user", null));
236
247
  },
237
248
  resolveWorkSession: function (args) {
249
+ if (canonicalClay) throw new Error("Work sessions require the existing project Driver workflow.");
238
250
  var state = current();
239
251
  var entry = visibleIssue(openStore(state.status), args.ref, state);
240
252
  if (!(entry.linkedWorkSessions || []).some(function (link) { return link.sessionId === args.sessionOriginId; })) throw new Error("Work session unavailable.");
@@ -255,9 +267,9 @@ function attachIssuesService(ctx) {
255
267
  readIssueTombstone: function (args) { var state = current(); return scopedIssue(openStore(state.status), args && args.ref, state); },
256
268
  issueHistory: function (args) { var state = current(); var store = openStore(state.status); var history = store.history(args && args.ref); if (state.worktreeOnly) { var historical = scopedIssue(store, args && args.ref, state); if (!historical.context || historical.context.changeSetId !== state.status.changeSetId) throw new Error("Issue not found."); } return history; },
257
269
  readIssueRevision: function (args) { var state = current(); var store = openStore(state.status); visibleIssue(store, args && args.ref, state); return store.readRevision(args.ref, args.revision); },
258
- commentIssue: function (args) { var state = current(); var target = openStore(state.status); visibleIssue(target, args && args.ref, state); return target.comment(args.ref, args.body, author(state.user, "user", null)); },
270
+ commentIssue: function (args) { var state = current(); var target = openStore(state.status); expected(visibleIssue(target, args && args.ref, state), args); return target.comment(args.ref, args.body, canonicalClay ? actor(state) : author(state.user, "user", null)); },
259
271
  listIssueFeedback: function (args) {
260
- if (!isSession) throw new Error("Only the Project Driver can review issue comments.");
272
+ if (!isSession && !canonicalClay) throw new Error("Only the Project Driver can review issue comments.");
261
273
  var state = current(); var target = openStore(state.status); var out = []; var feedbackLimit = args && args.limit === undefined ? 200 : Number(args.limit);
262
274
  if (!Number.isInteger(feedbackLimit) || feedbackLimit < 1 || feedbackLimit > 200) throw new Error("Feedback limit must be between 1 and 200.");
263
275
  var position = { issueCursor: null, issueIndex: 0, commentOffset: 0 };
@@ -297,31 +309,32 @@ function attachIssuesService(ctx) {
297
309
  return { feedback: out, total: out.length, nextCursor: nextCursor, truncated: !!nextCursor };
298
310
  },
299
311
  reviewIssueComment: function (args) {
300
- if (!isSession) throw new Error("Only the Project Driver can review issue comments.");
312
+ if (!isSession && !canonicalClay) throw new Error("Only the Project Driver can review issue comments.");
301
313
  var state = current(); var target = openStore(state.status); var entry = visibleIssue(target, args && args.ref, state);
314
+ expected(entry, args);
302
315
  if (args.action === "incorporate") {
303
316
  if (!Number.isInteger(args.expectedRevision)) throw new Error("expectedRevision is required to incorporate a comment.");
304
317
  var data = cleanPublic(args, { ref: true, expectedRevision: true, commentId: true, action: true, response: true, commitSha: true }, true);
305
318
  if (data.status === "resolved") throw new Error("Use update_issue with verified commit evidence before resolving this Issue.");
306
- return target.incorporate(entry.ref, args.commentId, data, args.expectedRevision, args.response, author(state.user, "session", state.session));
319
+ return target.incorporate(entry.ref, args.commentId, data, args.expectedRevision, args.response, actor(state));
307
320
  }
308
- return target.review(entry.ref, args.commentId, args.action, args.response, author(state.user, "session", state.session));
321
+ return target.review(entry.ref, args.commentId, args.action, args.response, actor(state));
309
322
  },
310
323
  removeIssue: function (args) {
311
324
  var state = current();
312
325
  if (!state.principal.isProjectOwner) throw new Error("Only the project owner can delete Issues.");
313
326
  var target = openStore(state.status); var entry = visibleIssue(target, args && args.ref, state);
314
327
  if (!Number.isInteger(args && args.expectedRevision) || entry.revision !== args.expectedRevision) throw new Error("Issue revision conflict.");
315
- return target.remove(entry.ref, author(state.user, "user", null), args.expectedRevision);
328
+ return target.remove(entry.ref, canonicalClay ? actor(state) : author(state.user, "user", null), args.expectedRevision);
316
329
  },
317
330
  createIssue: function (args) {
318
- if (!isSession) throw new Error("People cannot create issues directly. Ask the Project Driver to create one.");
331
+ if (!isSession && !canonicalClay) throw new Error("People cannot create issues directly. Ask the Project Driver to create one.");
319
332
  var data = cleanPublic(args, { commitSha: true }, true);
320
333
  var status = data.status === undefined ? "open" : data.status;
321
334
  if (schema.STATUSES.indexOf(status) === -1) throw new Error("Invalid status.");
322
335
  var state = current();
323
336
  if (status !== "resolved") return Promise.resolve(openStore(state.status).create(data,
324
- author(state.user, state.session ? "session" : "user", state.session), issueContext(state.status)));
337
+ actor(state), issueContext(state.status)));
325
338
  if (typeof data.resolutionSummary !== "string" || !data.resolutionSummary.trim()) throw new Error("resolutionSummary is required when resolving an issue.");
326
339
  var before = operation(state, true);
327
340
  return evidence(before, args.commitSha).then(function (proof) {
@@ -340,7 +353,7 @@ function attachIssuesService(ctx) {
340
353
  var existing = visibleIssue(target, ref, state);
341
354
  if (existing.revision !== expectedRevision) throw new Error("Issue revision conflict.");
342
355
  if (data.status !== "resolved") return Promise.resolve(target.update(ref, data, expectedRevision,
343
- author(state.user, state.session ? "session" : "user", state.session)));
356
+ actor(state)));
344
357
  if (typeof data.resolutionSummary !== "string" || !data.resolutionSummary.trim()) throw new Error("resolutionSummary is required when resolving an issue.");
345
358
  var before = operation(state, true);
346
359
  return evidence(before, args.commitSha).then(function (proof) {
@@ -354,6 +367,7 @@ function attachIssuesService(ctx) {
354
367
  }
355
368
 
356
369
  return { bindUser: function (source) { return bind(source, false); },
370
+ bindCanonicalClay: function (source) { return bind(source, false, true); },
357
371
  bindProjectSession: function (source) { return bind(source, true); },
358
372
  issueScopeIdForKnowledgeId: issuesStore.issueScopeIdForKnowledgeId };
359
373
  }
@@ -38,9 +38,10 @@ function attachKnowledgeDbImport(ctx) {
38
38
  }
39
39
  function importRecords(args) {
40
40
  // Snapshot caller-owned JSON before any asynchronous operation.
41
- var batch = ctx.clone(input.batch(args)); args = ctx.clone(args);
41
+ var revalidate = args.revalidate; var batch = ctx.clone(input.batch(args)); args = ctx.clone(args); args.revalidate = revalidate;
42
42
  var operationId = ctx.mutationId(args.operationId); var fingerprint = ctx.fingerprint("import", args);
43
43
  return ctx.serialize(args.databaseId, function () { return ctx.load(args.databaseId).then(async function (current) {
44
+ ctx.guard(args, current);
44
45
  var duplicate = ctx.receipt(current, operationId, fingerprint);
45
46
  if (duplicate) return Object.assign({}, duplicate, { projectionPending: current.projectionPending === true, projectionError: current.projectionError || null });
46
47
  if (current.archivedAt) throw ctx.error("Knowledge database is archived.", "ARCHIVED", current.revision);
@@ -60,10 +61,10 @@ function attachKnowledgeDbImport(ctx) {
60
61
  // payload still conflicts. Preserve record values, archives and database revision.
61
62
  if (!added.length) {
62
63
  ctx.remember(next, operationId, fingerprint, result);
63
- await ctx.replace(current, next);
64
+ await ctx.replace(current, next, args);
64
65
  return Object.assign(result, { projectionPending: current.projectionPending === true, projectionError: current.projectionError || null });
65
66
  }
66
- var databases = await ctx.all(); var at = Date.now(); var who = ctx.actor(args.actor);
67
+ var databases = await ctx.all(); ctx.guard(args, current); var at = Date.now(); var who = ctx.actor(args.actor);
67
68
  added.forEach(function (item) {
68
69
  next.records.push({ id: item.mapping.recordId, revision: 1, values: {}, archivedAt: null, createdAt: at, updatedAt: at, createdBy: who, updatedBy: who });
69
70
  next.recordHistory[item.mapping.recordId] = [];
@@ -78,7 +79,7 @@ function attachKnowledgeDbImport(ctx) {
78
79
  next.revision++; next.updatedAt = at; next.updatedBy = who; next.projectionPending = true; next.projectionError = null;
79
80
  result.databaseRevision = next.revision;
80
81
  ctx.remember(next, operationId, fingerprint, result);
81
- await ctx.replace(current, next);
82
+ await ctx.replace(current, next, args);
82
83
  return ctx.finish(next, result, who);
83
84
  }); });
84
85
  }