clay-server 5.3.0-beta.4 → 5.3.0-beta.6

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 (144) hide show
  1. package/lib/ask-user-request-identity.js +18 -1
  2. package/lib/clay-product-control-tools.js +2 -2
  3. package/lib/clay-product-control.js +1 -1
  4. package/lib/clay-screen-controls.js +1 -0
  5. package/lib/clay-split-view-awareness.js +5 -2
  6. package/lib/clay-split-view-controls.js +14 -7
  7. package/lib/clay-split-view-input.js +5 -4
  8. package/lib/clay-split-view-render-proof.js +14 -4
  9. package/lib/clay-view-workspace-input.js +71 -0
  10. package/lib/project-delegated-follow-up.js +18 -9
  11. package/lib/project-file-history.js +11 -9
  12. package/lib/project-file-watch.js +26 -7
  13. package/lib/project-filesystem.js +5 -4
  14. package/lib/project-shared-browser.js +7 -3
  15. package/lib/project-split-view-document.js +16 -2
  16. package/lib/project-split-views.js +15 -294
  17. package/lib/project-user-message.js +4 -3
  18. package/lib/project-vendor-login.js +2 -2
  19. package/lib/project-view-input.js +11 -0
  20. package/lib/project-view-pair.js +26 -0
  21. package/lib/project-view-store.js +346 -0
  22. package/lib/project-view-terminal-binding.js +16 -0
  23. package/lib/project-view-workspace.js +126 -0
  24. package/lib/project.js +1 -0
  25. package/lib/public/app.js +2 -0
  26. package/lib/public/css/issues.css +3 -0
  27. package/lib/public/css/pane.css +5 -1
  28. package/lib/public/css/right-workbench.css +25 -3
  29. package/lib/public/css/scheduled-tasks.css +7 -0
  30. package/lib/public/css/shared-browser.css +21 -0
  31. package/lib/public/css/split-view-layout.css +37 -3
  32. package/lib/public/css/sticky-notes.css +3 -2
  33. package/lib/public/css/tool-pane.css +127 -0
  34. package/lib/public/index.html +3 -3
  35. package/lib/public/modules/app-header.js +24 -2
  36. package/lib/public/modules/app-messages.js +1 -1
  37. package/lib/public/modules/clay-file-links.js +2 -1
  38. package/lib/public/modules/clay-session-links.js +1 -1
  39. package/lib/public/modules/clay-split-view-screen.js +6 -3
  40. package/lib/public/modules/clay-tool-pane-screen.js +35 -0
  41. package/lib/public/modules/file-history-snapshot.js +12 -0
  42. package/lib/public/modules/file-pane-entrypoints.js +55 -0
  43. package/lib/public/modules/file-pane-history.js +78 -0
  44. package/lib/public/modules/file-pane-instances.js +140 -0
  45. package/lib/public/modules/file-pane-links.js +41 -0
  46. package/lib/public/modules/file-pane-render.js +48 -0
  47. package/lib/public/modules/file-pane-review.js +26 -0
  48. package/lib/public/modules/file-pane-toolbar.js +54 -0
  49. package/lib/public/modules/file-viewer-source.js +70 -0
  50. package/lib/public/modules/filebrowser.js +44 -83
  51. package/lib/public/modules/git-panel.js +5 -0
  52. package/lib/public/modules/issues.js +3 -4
  53. package/lib/public/modules/linear-panel.js +2 -2
  54. package/lib/public/modules/mate-knowledge-workbench.js +10 -6
  55. package/lib/public/modules/notifications.js +4 -1
  56. package/lib/public/modules/pane-activation.js +5 -0
  57. package/lib/public/modules/pane-binding.js +5 -1
  58. package/lib/public/modules/pane-browser-bridge.js +20 -6
  59. package/lib/public/modules/pane-browser-transport.js +110 -0
  60. package/lib/public/modules/pane-header-runtime.js +18 -0
  61. package/lib/public/modules/plantuml-preview.js +12 -12
  62. package/lib/public/modules/right-workbench.js +116 -67
  63. package/lib/public/modules/scheduled-tasks.js +7 -6
  64. package/lib/public/modules/shared-browser.js +75 -9
  65. package/lib/public/modules/split-pane-renderer.js +7 -8
  66. package/lib/public/modules/split-pane-runtime.js +23 -0
  67. package/lib/public/modules/split-view-drag-preview.js +90 -0
  68. package/lib/public/modules/split-view-layout-client.js +1 -1
  69. package/lib/public/modules/split-view-presentation-drag.js +36 -13
  70. package/lib/public/modules/split-view-presentation-drop.js +21 -18
  71. package/lib/public/modules/split-view-presentation-renderer.js +15 -10
  72. package/lib/public/modules/split-view-presentations.js +25 -33
  73. package/lib/public/modules/split-view.js +17 -16
  74. package/lib/public/modules/sticky-notes-browser.js +2 -2
  75. package/lib/public/modules/sticky-notes.js +8 -10
  76. package/lib/public/modules/terminal.js +45 -13
  77. package/lib/public/modules/theme.js +1 -1
  78. package/lib/public/modules/tool-pane-chrome.js +82 -0
  79. package/lib/public/modules/tool-pane-drag.js +134 -0
  80. package/lib/public/modules/tool-pane-layout.js +95 -0
  81. package/lib/public/modules/tool-pane-legacy.js +60 -0
  82. package/lib/public/modules/tool-pane-tree.js +51 -0
  83. package/lib/public/modules/view-legacy-coordinates.js +16 -0
  84. package/lib/public/modules/view-mutations.js +24 -0
  85. package/lib/public/modules/view-pair-presentation.js +74 -0
  86. package/lib/public/modules/view-pair-validation.js +12 -0
  87. package/lib/public/modules/view-presentation-state.js +37 -0
  88. package/lib/public/modules/view-workspace-controller.js +210 -0
  89. package/lib/public/modules/view-workspace-state.js +103 -0
  90. package/lib/public/modules/view-workspace-validation.js +47 -0
  91. package/lib/public/modules/wireframe-workbench.js +11 -0
  92. package/lib/public/modules/workbench-layout-preference.js +12 -5
  93. package/lib/public/modules/workbench-panels.js +21 -0
  94. package/lib/public/modules/workspace-assignment-card.js +9 -6
  95. package/lib/public/pane-assets/{app-ED2VCGNQ.js → app-7UZ727NA.js} +1 -1
  96. package/lib/public/pane-assets/chunk-2AIWCI6Y.js +1 -0
  97. package/lib/public/pane-assets/chunk-6MIHIQ2W.js +1 -0
  98. package/lib/public/pane-assets/chunk-6NYCNWVF.js +1 -0
  99. package/lib/public/pane-assets/chunk-6QFLYOVY.js +1 -0
  100. package/lib/public/pane-assets/chunk-6UJ2ALOB.js +1 -0
  101. package/lib/public/pane-assets/chunk-6XK2OATZ.js +1 -0
  102. package/lib/public/pane-assets/chunk-BKIDR2ME.js +1 -0
  103. package/lib/public/pane-assets/chunk-BMHTBRMO.js +1 -0
  104. package/lib/public/pane-assets/chunk-GEFCFZEN.js +1 -0
  105. package/lib/public/pane-assets/chunk-GN2NQEVC.js +2 -0
  106. package/lib/public/pane-assets/chunk-IMLMBPT4.js +166 -0
  107. package/lib/public/pane-assets/chunk-JIV7O5GN.js +1 -0
  108. package/lib/public/pane-assets/chunk-K5S6AILM.js +1 -0
  109. package/lib/public/pane-assets/chunk-LDIZLKB2.js +1 -0
  110. package/lib/public/pane-assets/chunk-OWNNLZ36.js +1 -0
  111. package/lib/public/pane-assets/chunk-P2C3LOWL.js +3 -0
  112. package/lib/public/pane-assets/chunk-PV4PCH2V.js +2 -0
  113. package/lib/public/pane-assets/chunk-R4LMPTKU.js +1 -0
  114. package/lib/public/pane-assets/chunk-R7QVVFUG.js +1 -0
  115. package/lib/public/pane-assets/chunk-TMGSDFJI.js +1 -0
  116. package/lib/public/pane-assets/chunk-VKYBAVWT.js +1 -0
  117. package/lib/public/pane-assets/chunk-VNAV23CM.js +1 -0
  118. package/lib/public/pane-assets/chunk-VWTHIQCE.js +1 -0
  119. package/lib/public/pane-assets/chunk-VZ2OKDAO.js +1 -0
  120. package/lib/public/pane-assets/chunk-WM3K5AFH.js +1 -0
  121. package/lib/public/pane-assets/chunk-XOB3O327.js +1 -0
  122. package/lib/public/pane-assets/chunk-XOOUCFYT.js +1 -0
  123. package/lib/public/pane-assets/manifest.json +383 -336
  124. package/lib/public/pane-assets/style-ZM7OOU3M.css +1 -0
  125. package/lib/public/style.css +1 -0
  126. package/lib/server-workbench-layout-settings.js +3 -3
  127. package/lib/server.js +1 -0
  128. package/lib/split-view-layout.js +11 -7
  129. package/lib/users-workbench-layout-preferences.js +14 -4
  130. package/lib/workbench-window-layout.js +30 -0
  131. package/lib/workspace-assignment-service.js +56 -11
  132. package/lib/workspace-follow-up-queue.js +92 -0
  133. package/lib/workspace-query-mcp-server.js +1 -1
  134. package/package.json +1 -1
  135. package/lib/public/pane-assets/chunk-6D4DPE5X.js +0 -1
  136. package/lib/public/pane-assets/chunk-6SM2WQJX.js +0 -1
  137. package/lib/public/pane-assets/chunk-7H36SUZA.js +0 -2
  138. package/lib/public/pane-assets/chunk-DGOFGEUK.js +0 -1
  139. package/lib/public/pane-assets/chunk-I57URYSL.js +0 -1
  140. package/lib/public/pane-assets/chunk-IYFILQHZ.js +0 -2
  141. package/lib/public/pane-assets/chunk-IZ2ZANEY.js +0 -167
  142. package/lib/public/pane-assets/chunk-JAEQCN5S.js +0 -1
  143. package/lib/public/pane-assets/chunk-ST2Q62ET.js +0 -1
  144. package/lib/public/pane-assets/style-JVCOCG3W.css +0 -1
@@ -79,6 +79,7 @@
79
79
  @import url("css/skills-workbench.css");
80
80
 
81
81
  @import url("css/right-workbench.css?v=20261020-workbench-overlay");
82
+ @import url("css/tool-pane.css");
82
83
 
83
84
  @import url("css/mate-conversation.css");
84
85
 
@@ -32,7 +32,7 @@ function attachWorkbenchLayoutSettings(ctx) {
32
32
  req.on('data', function (chunk) {
33
33
  if (finished) return;
34
34
  body += chunk;
35
- if (Buffer.byteLength(body) > 1024) { finished = true; body = ''; reply(res, 413, { error: 'Request too large.' }); }
35
+ if (Buffer.byteLength(body) > 65536) { finished = true; body = ''; reply(res, 413, { error: 'Request too large.' }); }
36
36
  });
37
37
  req.on('aborted', function () { finished = true; });
38
38
  req.on('error', function () { finished = true; });
@@ -41,11 +41,11 @@ function attachWorkbenchLayoutSettings(ctx) {
41
41
  if (identity(req) !== accountId) { reply(res, 401, { error: 'Account changed. Sign in again.' }); return; }
42
42
  var input;
43
43
  try { input = JSON.parse(body); } catch (error) { reply(res, 400, { error: 'Invalid JSON.' }); return; }
44
- if (!input || Array.isArray(input) || Object.keys(input).length !== 2 || !Object.prototype.hasOwnProperty.call(input, 'mode') || !Object.prototype.hasOwnProperty.call(input, 'revision')) {
44
+ if (!input || Array.isArray(input) || Object.keys(input).some(function (key) { return ['mode', 'revision', 'layout'].indexOf(key) < 0; }) || !Object.prototype.hasOwnProperty.call(input, 'mode') || !Object.prototype.hasOwnProperty.call(input, 'revision')) {
45
45
  reply(res, 400, { error: 'Invalid workbench preference.' }); return;
46
46
  }
47
47
  try {
48
- var result = ctx.users.setWorkbenchLayoutPreference(accountId, input.mode, input.revision);
48
+ var result = ctx.users.setWorkbenchLayoutPreference(accountId, input.mode, input.revision, input.layout);
49
49
  reply(res, result.error ? result.status || 400 : 200, result);
50
50
  } catch (error) { reply(res, 500, { error: 'Workbench layout could not be saved. Your previous layout is retained.' }); }
51
51
  });
package/lib/server.js CHANGED
@@ -317,6 +317,7 @@ function createServer(opts) {
317
317
 
318
318
  // --- Project registry ---
319
319
  var assignmentService = attachWorkspaceAssignmentService({
320
+ resolveMate: function (owner, id) { return mates.getMate(mates.buildMateCtx(users.isMultiUser() ? owner : null), id); },
320
321
  getPresentation: function (principal) {
321
322
  var project = projects.get(principal.sourceProjectSlug), manager = project && project.getSessionManager(), status = project && project.getStatus();
322
323
  var session = manager && manager.sessions.get(principal.sourceSessionId);
@@ -12,7 +12,7 @@ function validateTree(tree, expectedOrigins) {
12
12
  var invalid = null;
13
13
  function walk(node, depth) {
14
14
  if (invalid) return;
15
- if (!node || typeof node !== "object" || depth > MAX_SESSIONS) { invalid = "Layout tree is malformed"; return; }
15
+ if (!node || typeof node !== "object" || Array.isArray(node) || depth > MAX_SESSIONS) { invalid = "Layout tree is malformed"; return; }
16
16
  if (Object.prototype.hasOwnProperty.call(node, "id")) {
17
17
  if (Object.keys(node).length !== 1 || !validOrigin(node.id) || origins.indexOf(node.id) !== -1) {
18
18
  invalid = "Layout leaves must have unique valid session origins"; return;
@@ -28,11 +28,11 @@ function validateTree(tree, expectedOrigins) {
28
28
  }
29
29
  walk(node.left, depth + 1); walk(node.right, depth + 1);
30
30
  }
31
- walk(tree, 0);
31
+ if (tree !== null) walk(tree, 0);
32
32
  if (invalid) return { ok: false, error: invalid, origins: origins };
33
- if (origins.length < 2 || origins.length > MAX_SESSIONS) return { ok: false, error: "A Split view requires two to eight sessions", origins: origins };
33
+ if (origins.length > MAX_SESSIONS) return { ok: false, error: "A View supports zero to eight sessions", origins: origins };
34
34
  if (expectedOrigins) {
35
- if (!Array.isArray(expectedOrigins) || expectedOrigins.length !== origins.length ||
35
+ if (!Array.isArray(expectedOrigins) || expectedOrigins.length !== origins.length || new Set(expectedOrigins).size !== expectedOrigins.length ||
36
36
  expectedOrigins.some(function (id) { return !validOrigin(id) || origins.indexOf(id) === -1; })) {
37
37
  return { ok: false, error: "Layout leaves must exactly match the selected sessions", origins: origins };
38
38
  }
@@ -100,9 +100,9 @@ function validateViewShape(view) {
100
100
  var name = validateName(view.name);
101
101
  if (!name.ok) return name;
102
102
  if (view.nameMode != null && ["automatic", "custom"].indexOf(view.nameMode) === -1) return { ok: false, error: "Split view name mode is invalid" };
103
- if (!Number.isInteger(view.revision) || view.revision < 1 || !validatePreset(view.layout)) return { ok: false, error: "Split view revision or layout is invalid" };
103
+ if (!Number.isSafeInteger(view.revision) || view.revision < 1 || !validatePreset(view.layout)) return { ok: false, error: "Split view revision or layout is invalid" };
104
104
  var refs = view.sessionRefs;
105
- if (!Array.isArray(refs) || refs.length < 2 || refs.length > MAX_SESSIONS) return { ok: false, error: "A Split view requires two to eight sessions" };
105
+ if (!Array.isArray(refs) || refs.length > MAX_SESSIONS) return { ok: false, error: "A View supports zero to eight sessions" };
106
106
  var ids = [];
107
107
  for (var i = 0; i < refs.length; i++) {
108
108
  var ref = refs[i];
@@ -113,7 +113,11 @@ function validateViewShape(view) {
113
113
  }
114
114
  var tree = validateTree(view.tree, ids);
115
115
  if (!tree.ok) return tree;
116
- if (typeof view.activeSessionOriginId !== "string" || ids.indexOf(view.activeSessionOriginId) === -1) return { ok: false, error: "Active session must belong to the Split view" };
116
+ if (ids.length ? typeof view.activeSessionOriginId !== "string" || ids.indexOf(view.activeSessionOriginId) === -1 : view.activeSessionOriginId !== null) return { ok: false, error: "Active session must belong to the Split view" };
117
+ if (view.workspace !== undefined) {
118
+ var checked = require("./project-view-workspace").validate(view.workspace, view);
119
+ if (!checked.ok) return checked;
120
+ }
117
121
  return { ok: true, name: name.name, origins: ids };
118
122
  }
119
123
 
@@ -1,3 +1,4 @@
1
+ var windowLayouts = require('./workbench-window-layout');
1
2
  // Account UI preferences are excluded from canonical Clay controls.
2
3
  function validMode(mode) { return mode === 'overlay' || mode === 'docked'; }
3
4
  function attachWorkbenchLayoutPreferences(deps) {
@@ -12,16 +13,25 @@ function attachWorkbenchLayoutPreferences(deps) {
12
13
  function value(accountId, saved) {
13
14
  if (saved === undefined) return { accountId: accountId, mode: 'overlay', revision: 0 };
14
15
  if (!saved || !validMode(saved.mode) || !Number.isSafeInteger(saved.revision) || saved.revision < 1) throw new Error('Invalid stored preference.');
15
- return { accountId: accountId, mode: saved.mode, revision: saved.revision };
16
+ if (saved.layouts && !windowLayouts.validLayouts(saved.layouts)) throw new Error('Invalid stored window layout.');
17
+ return Object.assign({ accountId: accountId, mode: saved.mode, revision: saved.revision }, saved.layouts ? { layouts: saved.layouts } : {});
16
18
  }
17
19
  function get(accountId) { return value(accountId, context(accountId).saved); }
18
- function set(accountId, mode, revision) {
20
+ function set(accountId, mode, revision, layout) {
21
+ if (layout !== undefined && !windowLayouts.validLayout(layout)) return { status: 400, error: 'Invalid window layout.' };
19
22
  if (!validMode(mode) || !Number.isSafeInteger(revision) || revision < 0) return { status: 400, error: 'Invalid workbench layout.' };
20
23
  var current = context(accountId), prior = value(accountId, current.saved);
21
24
  if (prior.revision !== revision) return { status: 409, error: 'Workbench layout changed elsewhere. Reload the preference and try again.' };
22
- if (prior.mode === mode) return prior;
25
+ var layouts = Object.assign({}, prior.layouts || {});
26
+ var changed = layout && JSON.stringify(layouts[layout.key]) !== JSON.stringify({ base: layout.base, tree: layout.tree });
27
+ if (prior.mode === mode && !changed) return prior;
28
+ if (layout) {
29
+ delete layouts[layout.key];
30
+ if (Object.keys(layouts).length >= 32) delete layouts[Object.keys(layouts)[0]];
31
+ layouts[layout.key] = { base: layout.base, tree: layout.tree };
32
+ }
23
33
  if (revision === Number.MAX_SAFE_INTEGER) throw new Error('Preference revision exhausted.');
24
- var saved = { mode: mode, revision: revision + 1 };
34
+ var saved = Object.assign({ mode: mode, revision: revision + 1 }, Object.keys(layouts).length ? { layouts: layouts } : {});
25
35
  if (current.settings) config.saveConfig(Object.assign({}, current.settings, { workbenchLayoutPreference: saved }));
26
36
  else deps.saveUsers(Object.assign({}, current.data, { users: current.data.users.map(function (entry) {
27
37
  return entry === current.user ? Object.assign({}, entry, { workbenchLayoutPreference: saved }) : entry;
@@ -0,0 +1,30 @@
1
+ // Display preferences only: these records never open sessions or run tools.
2
+ var tools = ['tool-terminal', 'tool-browser', 'tool-file-viewer'];
3
+ function validTree(tree, allowTools) {
4
+ var ids = [], nodes = 0;
5
+ function walk(node, depth) {
6
+ if (!node || typeof node !== 'object' || Array.isArray(node) || ++nodes > 21 || depth > 10) return false;
7
+ if (Object.prototype.hasOwnProperty.call(node, 'id')) {
8
+ if (Object.keys(node).length !== 1 || typeof node.id !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(node.id)) return false;
9
+ if (node.id.indexOf('tool-') === 0 && (!allowTools || tools.indexOf(node.id) < 0)) return false;
10
+ ids.push(node.id); return true;
11
+ }
12
+ return Object.keys(node).sort().join(',') === 'axis,left,ratio,right' && ['horizontal', 'vertical'].indexOf(node.axis) >= 0 &&
13
+ Number.isFinite(node.ratio) && node.ratio >= .05 && node.ratio <= .95 && walk(node.left, depth + 1) && walk(node.right, depth + 1);
14
+ }
15
+ return walk(tree, 0) && ids.length > 0 && ids.length <= (allowTools ? 11 : 8) && new Set(ids).size === ids.length;
16
+ }
17
+ function validLayout(input) {
18
+ return !!input && typeof input === 'object' && !Array.isArray(input) && Object.keys(input).sort().join(',') === 'base,key,tree' &&
19
+ typeof input.key === 'string' && input.key.length > 0 && input.key.length <= 512 && input.key !== '__proto__' &&
20
+ validTree(input.base, false) && validTree(input.tree, true) && sameSessions(input.base, input.tree);
21
+ }
22
+ function sameSessions(base, tree) {
23
+ function ids(node) { return node.id ? (tools.indexOf(node.id) >= 0 ? [] : [node.id]) : ids(node.left).concat(ids(node.right)); }
24
+ return JSON.stringify(ids(base).sort()) === JSON.stringify(ids(tree).sort());
25
+ }
26
+ function validLayouts(layouts) {
27
+ return !!layouts && typeof layouts === 'object' && !Array.isArray(layouts) && Object.keys(layouts).length <= 32 &&
28
+ Object.keys(layouts).every(function (key) { var item = layouts[key]; return item && validLayout({ key: key, base: item.base, tree: item.tree }); });
29
+ }
30
+ module.exports = { validLayout: validLayout, validLayouts: validLayouts };
@@ -2,7 +2,7 @@ var fs = require("fs");
2
2
  var path = require("path");
3
3
  var crypto = require("crypto");
4
4
 
5
- var ACTIVE = { proposed: true, starting: true, running: true };
5
+ var ACTIVE = { proposed: true, queued: true, starting: true, running: true };
6
6
  var MAX_RUNNING = 3;
7
7
 
8
8
  function clean(value, max) {
@@ -21,6 +21,8 @@ function attachWorkspaceAssignmentService(ctx) {
21
21
  var pendingRestartReplay = {};
22
22
  var presentations = new WeakMap();
23
23
  var presentationTargets = new WeakMap();
24
+ var followUps = require('./workspace-follow-up-queue').createFollowUpQueue({ records: records,
25
+ exactSource: exactSource, exactTarget: exactTarget, settle: settle, approve: approve });
24
26
 
25
27
  function recordFile(userId) {
26
28
  return storageDir ? path.join(storageDir, userStorageKey(userId)) : null;
@@ -44,12 +46,13 @@ function attachWorkspaceAssignmentService(ctx) {
44
46
  if (!files[i].endsWith(".json") || files[i].indexOf(".tmp.") !== -1) continue;
45
47
  try {
46
48
  var loaded = JSON.parse(fs.readFileSync(path.join(storageDir, files[i]), "utf8"));
47
- if (Array.isArray(loaded)) records = records.concat(loaded);
49
+ if (Array.isArray(loaded)) records.push.apply(records, loaded);
48
50
  } catch (e) {}
49
51
  }
50
52
  var changedUsers = {};
51
53
  for (var j = 0; j < records.length; j++) {
52
- if (records[j].status !== "starting" && records[j].status !== "running") continue;
54
+ if (records[j].status !== "queued" && records[j].status !== "starting" && records[j].status !== "running") continue;
55
+ records[j].deliveryState = records[j].status === "queued" ? "not_sent" : "unknown";
53
56
  records[j].status = "interrupted";
54
57
  records[j].error = "Clay restarted before this assignment completed.";
55
58
  records[j].updatedAt = Date.now();
@@ -90,6 +93,8 @@ function attachWorkspaceAssignmentService(ctx) {
90
93
  title: record.title,
91
94
  task: record.task,
92
95
  status: record.status,
96
+ deliveryState: record.deliveryState || null,
97
+ parentAssignmentId: record.parentAssignmentId || null,
93
98
  sourceProjectSlug: record.sourceProjectSlug,
94
99
  sourceSessionRef: record.sourceSessionRef,
95
100
  targetSessionRef: record.targetSessionRef || null,
@@ -119,6 +124,8 @@ function attachWorkspaceAssignmentService(ctx) {
119
124
  var status = project.getStatus();
120
125
  if (!status || status.isMate !== true || status.mateId !== record.sourceMateId) return null;
121
126
  if (ctx.isMultiUser() && status.projectOwnerId !== record.userId) return null;
127
+ var mate = ctx.resolveMate && ctx.resolveMate(status.projectOwnerId || null, record.sourceMateId);
128
+ if (ctx.resolveMate && (!mate || mate.id !== record.sourceMateId || ctx.isMultiUser() && mate.createdBy !== record.userId)) return null;
122
129
  var manager = project.getSessionManager();
123
130
  var session = null;
124
131
  if (manager && manager.sessions) {
@@ -129,7 +136,7 @@ function attachWorkspaceAssignmentService(ctx) {
129
136
  if (!session) return null;
130
137
  if (ctx.isMultiUser() && session.ownerId !== record.userId) return null;
131
138
  if (!ctx.isMultiUser() && session.ownerId && session.ownerId !== record.singleUserOwnerId) return null;
132
- return { project: project, manager: manager, session: session };
139
+ return { project: project, manager: manager, session: session, mate: mate || null };
133
140
  }
134
141
 
135
142
  function present(record, principal) {
@@ -149,7 +156,7 @@ function attachWorkspaceAssignmentService(ctx) {
149
156
  function emit(record, eventType) {
150
157
  var observer = presentations.get(record);
151
158
  if (observer) {
152
- if (record.status === 'starting') observer.phase('queued');
159
+ if (record.status === 'starting' || record.status === 'queued') observer.phase('queued');
153
160
  else if (record.status === 'running') observer.phase('running');
154
161
  else if (record.status !== 'proposed') { observer.finish({ status: record.status === 'interrupted' ? 'failed' : record.status, outcome: record.status === 'interrupted' ? 'uncertain' : undefined }); presentations.delete(record); }
155
162
  }
@@ -232,12 +239,16 @@ function attachWorkspaceAssignmentService(ctx) {
232
239
  var idempotency = crypto.createHash("sha256").update(key + "\u0000follow_up\u0000" + projectSlug + "\u0000" + targetSessionRef + "\u0000" + title + "\u0000" + task).digest("hex");
233
240
  for (var i = 0; i < records.length; i++) {
234
241
  if (records[i].idempotencyKey === idempotency) return publicRecord(records[i]);
235
- if (records[i].sourceKey === key && ACTIVE[records[i].status]) throw new Error("This conversation already has an active assignment.");
242
+ if (records[i].sourceKey === key && ACTIVE[records[i].status] &&
243
+ (records[i].targetProjectSlug !== projectSlug || records[i].targetSessionRef !== targetSessionRef)) throw new Error("An active assignment already exists. Send this update to that exact Driver session.");
236
244
  }
237
245
  var candidate = { userId: principal.userId || null, singleUserOwnerId: principal.singleUserOwnerId || null, targetProjectSlug: projectSlug };
238
246
  var target = exactTarget(candidate);
239
247
  if (!target || typeof target.inspectDelegatedFollowUp !== "function") throw new Error("Target project is unavailable for follow-ups.");
240
- var inspected = target.inspectDelegatedFollowUp({ userId: principal.userId }, targetSessionRef);
248
+ if (records.filter(function (item) { return item.sourceKey === key && item.delivery === 'follow_up' && ACTIVE[item.status]; }).length >= 16) throw new Error('There are already 16 pending follow-ups. Wait for or cancel a review.');
249
+ var candidateInput = { userId: principal.userId || null, sourceKey: key, targetProjectSlug: projectSlug, targetSessionRef: targetSessionRef,
250
+ sourceMateId: principal.mateId, sourceProjectSlug: principal.sourceProjectSlug, sourceSessionRef: principal.sourceSessionRef };
251
+ var inspected = followUps.inspect(candidateInput, target);
241
252
  var now = Date.now();
242
253
  var targetStatus = target.getStatus();
243
254
  var record = {
@@ -250,6 +261,7 @@ function attachWorkspaceAssignmentService(ctx) {
250
261
  title: title, task: task, delivery: "follow_up", status: "proposed", createdAt: now, updatedAt: now,
251
262
  };
252
263
  if (!exactSource(record)) throw new Error("Source conversation is no longer available.");
264
+ followUps.remember(record, target, inspected);
253
265
  records.push(record);
254
266
  saveUser(record.userId);
255
267
  present(record, principal);
@@ -279,7 +291,9 @@ function attachWorkspaceAssignmentService(ctx) {
279
291
 
280
292
  function settle(record, status, session, error) {
281
293
  if (!ACTIVE[record.status]) return publicRecord(record);
294
+ if (record.delivery === "follow_up" && record.deliveryState !== "accepted") record.deliveryState = record.status === "starting" ? "unknown" : "not_sent";
282
295
  record.status = status;
296
+ followUps.cleanup(record);
283
297
  record.resultSummary = status === "completed" ? summarize(session) : "";
284
298
  record.error = clean(error || "", 800);
285
299
  record.updatedAt = Date.now();
@@ -287,12 +301,19 @@ function attachWorkspaceAssignmentService(ctx) {
287
301
  delete unsubscribers[record.assignmentId];
288
302
  saveUser(record.userId);
289
303
  emit(record);
304
+ followUps.wake(record);
290
305
  return publicRecord(record);
291
306
  }
292
307
 
293
308
  function watch(record, manager, session) {
294
309
  var view = presentationTargets.get(record); if (view) { view.session = session; view.origin = session.sessionOriginId; var observer = presentations.get(record); if (observer && observer.target) observer.target({ kind: 'session', projectSlug: record.targetProjectSlug, sessionId: session.localId, sessionOriginId: session.sessionOriginId }); }
310
+ if (unsubscribers[record.assignmentId]) unsubscribers[record.assignmentId]();
295
311
  var unsubscribe = manager.subscribeSession(session.localId, function (event) {
312
+ if (record.status === 'queued') {
313
+ if (event.type === 'error') settle(record, 'failed', session, event.text || 'Driver failed before follow-up delivery.');
314
+ else if (event.type === 'done' || event.type === 'result') followUps.schedule(record);
315
+ return;
316
+ }
296
317
  if (event.type === "session_id") {
297
318
  record.targetSessionRef = ctx.sessionRef(record.targetProjectSlug, session);
298
319
  record.updatedAt = Date.now();
@@ -301,6 +322,7 @@ function attachWorkspaceAssignmentService(ctx) {
301
322
  } else if (event.type === "error") {
302
323
  settle(record, "failed", session, event.text || "Assignment failed.");
303
324
  } else if (event.type === "done" || event.type === "result") {
325
+ if (record.delivery === "follow_up") record.deliveryState = "accepted";
304
326
  settle(record, session._lastTurnInterrupted ? "interrupted" : "completed", session, "");
305
327
  }
306
328
  });
@@ -308,17 +330,32 @@ function attachWorkspaceAssignmentService(ctx) {
308
330
  }
309
331
 
310
332
  async function approve(record) {
311
- if (record.status !== "proposed") return publicRecord(record);
333
+ if (record.status !== "proposed" && record.status !== "queued") return publicRecord(record);
312
334
  var running = records.filter(function (item) {
313
335
  return item.userId === record.userId && (item.status === "starting" || item.status === "running");
314
336
  }).length;
315
- if (running >= MAX_RUNNING) throw new Error("Too many assignments are already running.");
316
337
  if (!exactSource(record)) throw new Error("Source conversation is no longer available.");
317
338
  var target = exactTarget(record);
318
339
  var followUp = record.delivery === "follow_up";
319
340
  if (!target || (followUp ? typeof target.dispatchDelegatedFollowUp !== "function" : typeof target.createDelegatedSession !== "function")) throw new Error("Target project is no longer available.");
341
+ if (followUp) {
342
+ var inspected;
343
+ try { inspected = followUps.validate(record, target, followUps.inspect(record, target)); }
344
+ catch (error) { return settle(record, 'failed', null, error.message); }
345
+ if (followUps.busy(inspected.session) || (followUps.first(record) && followUps.first(record) !== record)) {
346
+ var beforeQueue = Object.assign({}, record, { decision: record.decision, deliveryState: record.deliveryState, queueOrder: record.queueOrder });
347
+ if (record.status === 'proposed') record.queueOrder = records.reduce(function (max, item) { return Math.max(max, item.queueOrder || 0); }, 0) + 1;
348
+ record.decision = 'approve'; record.status = 'queued'; record.deliveryState = 'queued'; record.updatedAt = Date.now();
349
+ try { saveUser(record.userId); } catch (error) { Object.assign(record, beforeQueue); throw error; }
350
+ watch(record, target.getSessionManager(), inspected.session); followUps.track(record); emit(record); followUps.schedule(record);
351
+ return publicRecord(record);
352
+ }
353
+ }
354
+ if (running >= MAX_RUNNING) throw new Error("Too many assignments are already running.");
355
+ followUps.cleanup(record);
320
356
  record.decision = "approve";
321
357
  record.status = "starting";
358
+ if (followUp) record.deliveryState = "dispatching";
322
359
  record.updatedAt = Date.now();
323
360
  saveUser(record.userId);
324
361
  emit(record);
@@ -329,10 +366,11 @@ function attachWorkspaceAssignmentService(ctx) {
329
366
  sourceMateId: record.sourceMateId,
330
367
  sourceProjectSlug: record.sourceProjectSlug,
331
368
  sourceSessionRef: record.sourceSessionRef,
369
+ parentAssignmentId: record.parentAssignmentId || null,
332
370
  };
333
371
  var session;
334
372
  if (followUp) {
335
- session = await target.dispatchDelegatedFollowUp({ userId: record.userId }, record.targetSessionRef, record.task, metadata, function (existing) { watch(record, target.getSessionManager(), existing); });
373
+ session = await target.dispatchDelegatedFollowUp({ userId: record.userId, sourceMateId: record.sourceMateId, sourceProjectSlug: record.sourceProjectSlug, sourceSessionRef: record.sourceSessionRef }, record.targetSessionRef, record.task, metadata, function (existing) { followUps.validate(record, exactTarget(record), followUps.inspect(record, target)); watch(record, target.getSessionManager(), existing); });
336
374
  } else {
337
375
  session = await target.createDelegatedSession({ userId: record.userId }, record.task, metadata, function (created) { watch(record, target.getSessionManager(), created); });
338
376
  }
@@ -340,6 +378,7 @@ function attachWorkspaceAssignmentService(ctx) {
340
378
  record.targetSessionRef = ctx.sessionRef(record.targetProjectSlug, session);
341
379
  record.targetSessionId = session.localId;
342
380
  record.status = "running";
381
+ if (followUp) record.deliveryState = "accepted";
343
382
  record.updatedAt = Date.now();
344
383
  saveUser(record.userId);
345
384
  emit(record);
@@ -350,12 +389,17 @@ function attachWorkspaceAssignmentService(ctx) {
350
389
  }
351
390
 
352
391
  function cancel(record) {
353
- if (record.status !== "proposed") return publicRecord(record);
392
+ if (record.status !== "proposed" && record.status !== "queued") return publicRecord(record);
393
+ followUps.cleanup(record);
394
+ if (unsubscribers[record.assignmentId]) unsubscribers[record.assignmentId]();
395
+ delete unsubscribers[record.assignmentId];
396
+ record.deliveryState = 'not_sent';
354
397
  record.decision = "cancel";
355
398
  record.status = "cancelled";
356
399
  record.updatedAt = Date.now();
357
400
  saveUser(record.userId);
358
401
  emit(record);
402
+ followUps.wake(record);
359
403
  return publicRecord(record);
360
404
  }
361
405
 
@@ -396,6 +440,7 @@ function attachWorkspaceAssignmentService(ctx) {
396
440
  validateResponseSource(ws, msg, record, routedProjectSlug);
397
441
  var action = msg.action === "approve" ? "approve" : (msg.action === "cancel" ? "cancel" : "");
398
442
  if (!action) throw new Error("Choose approve or cancel.");
443
+ if (record.status === "queued" && action === "cancel") return cancel(record);
399
444
  if (record.status !== "proposed") {
400
445
  if (record.decision === action) return publicRecord(record);
401
446
  throw new Error("Assignment proposal has already been resolved.");
@@ -0,0 +1,92 @@
1
+ // Reviewed assignment updates share the existing follow-up dispatch service.
2
+ function createFollowUpQueue(ctx) {
3
+ var snapshots = new WeakMap(), scheduled = new WeakSet(), sourceWatches = new WeakMap();
4
+ function source(record) {
5
+ var current = ctx.exactSource(record);
6
+ if (!current || current.session.taskStopRequested || current.session._pendingMessageDrainPaused || current.session._lastTurnInterrupted || current.session.abortController && current.session.abortController.signal.aborted || current.session._mentionInProgress || current.session.spawn) throw new Error('Source conversation stopped or changed. Request a new review.');
7
+ return current;
8
+ }
9
+ function inspect(record, project) {
10
+ var parent = ctx.records.find(function (item) {
11
+ return item.sourceKey === record.sourceKey && item.delivery !== 'follow_up' && item.targetProjectSlug === record.targetProjectSlug &&
12
+ item.targetSessionRef === record.targetSessionRef && (!record.parentAssignmentId || item.assignmentId === record.parentAssignmentId);
13
+ });
14
+ return project.inspectDelegatedFollowUp({ userId: record.userId, sourceMateId: record.sourceMateId,
15
+ sourceProjectSlug: record.sourceProjectSlug, sourceSessionRef: record.sourceSessionRef }, record.targetSessionRef,
16
+ { allowBusy: true, parentAssignmentId: parent && parent.assignmentId || null });
17
+ }
18
+ function remember(record, project, inspected) {
19
+ var current = source(record);
20
+ snapshots.set(record, { project: project, session: inspected.session, origin: inspected.session.sessionOriginId,
21
+ linuxUser: inspected.linuxUser || null, source: current.session, sourceProject: current.project });
22
+ record.targetSessionOriginId = inspected.session.sessionOriginId || null;
23
+ record.targetLinuxUser = inspected.linuxUser || null;
24
+ record.targetDirectory = project.getStatus().path || null;
25
+ record.sourceDirectory = current.project.getStatus().path || null;
26
+ record.sourceVisibility = current.session.sessionVisibility || null;
27
+ record.sourceBuiltinKey = current.mate && current.mate.builtinKey || null;
28
+ record.sourceSessionOriginId = current.session.sessionOriginId || null;
29
+ record.sourceQueryGeneration = current.session._sdkQueryGeneration || null;
30
+ var parent = ctx.records.find(function (item) { return item.sourceKey === record.sourceKey && item.delivery !== 'follow_up' &&
31
+ item.targetProjectSlug === record.targetProjectSlug && item.targetSessionRef === record.targetSessionRef; });
32
+ record.parentAssignmentId = parent && parent.assignmentId || null;
33
+ }
34
+ function validate(record, project, inspected) {
35
+ var current = source(record), saved = snapshots.get(record);
36
+ if (saved && (saved.project !== project || saved.session !== inspected.session || saved.origin !== inspected.session.sessionOriginId ||
37
+ saved.linuxUser !== (inspected.linuxUser || null) || saved.source !== current.session || saved.sourceProject !== current.project) ||
38
+ record.targetSessionOriginId && record.targetSessionOriginId !== inspected.session.sessionOriginId ||
39
+ record.sourceSessionOriginId && record.sourceSessionOriginId !== current.session.sessionOriginId ||
40
+ record.parentAssignmentId && (!inspected.session.assignment || inspected.session.assignment.assignmentId !== record.parentAssignmentId) ||
41
+ record.sourceVisibility !== (current.session.sessionVisibility || null) ||
42
+ record.targetDirectory !== (project.getStatus().path || null) || record.sourceDirectory !== (current.project.getStatus().path || null) ||
43
+ record.sourceBuiltinKey !== (current.mate && current.mate.builtinKey || null) ||
44
+ (record.targetLinuxUser || null) !== (inspected.linuxUser || null) ||
45
+ (record.sourceQueryGeneration || null) !== (current.session._sdkQueryGeneration || null)) throw new Error('Follow-up identity or OS mapping changed. Request a new review.');
46
+ return inspected;
47
+ }
48
+ function busy(session) { return !!(session.isProcessing || session._queryStarting || session._awaitingTurnResult); }
49
+ function cleanup(record) {
50
+ var unsubscribe = sourceWatches.get(record);
51
+ if (unsubscribe) unsubscribe();
52
+ sourceWatches.delete(record);
53
+ }
54
+ function track(record) {
55
+ if (sourceWatches.has(record)) return;
56
+ var current = source(record);
57
+ if (typeof current.manager.subscribeSession !== 'function') return;
58
+ var unsubscribe = current.manager.subscribeSession(current.session.localId, function () {
59
+ if (record.status !== 'queued') return;
60
+ try { validate(record, ctx.exactTarget(record), inspect(record, ctx.exactTarget(record))); } catch (error) { stop(record, 'interrupted', error); }
61
+ });
62
+ if (unsubscribe) sourceWatches.set(record, unsubscribe);
63
+ }
64
+ function first(record) {
65
+ return ctx.records.filter(function (item) { return item.status === 'queued' && item.userId === record.userId &&
66
+ item.targetProjectSlug === record.targetProjectSlug && item.targetSessionRef === record.targetSessionRef; })
67
+ .sort(function (a, b) { return a.queueOrder - b.queueOrder; })[0];
68
+ }
69
+ function wake(record) { var next = first(record); if (next) schedule(next); }
70
+ function stop(record, status, error) {
71
+ try { ctx.settle(record, status, null, error.message); }
72
+ catch (failure) { console.error('Follow-up status could not be persisted:', failure.message); }
73
+ }
74
+ function schedule(record) {
75
+ if (record.status !== 'queued' || scheduled.has(record)) return;
76
+ scheduled.add(record);
77
+ setImmediate(function () {
78
+ scheduled.delete(record);
79
+ if (record.status !== 'queued') return;
80
+ try {
81
+ var project = ctx.exactTarget(record);
82
+ if (!project) throw new Error('Target project access changed.');
83
+ var inspected = validate(record, project, inspect(record, project));
84
+ if (inspected.session._lastTurnInterrupted) throw new Error('Driver was stopped; explicit owner resumption is required.');
85
+ if (first(record) !== record || busy(inspected.session)) return;
86
+ Promise.resolve(ctx.approve(record)).catch(function (error) { stop(record, 'failed', error); });
87
+ } catch (error) { stop(record, 'interrupted', error); }
88
+ });
89
+ }
90
+ return { inspect: inspect, remember: remember, validate: validate, busy: busy, track: track, schedule: schedule, first: first, wake: wake, cleanup: cleanup };
91
+ }
92
+ module.exports = { createFollowUpQueue: createFollowUpQueue };
@@ -90,7 +90,7 @@ function commonTools(bound) {
90
90
  },
91
91
  {
92
92
  name: "propose_project_follow_up",
93
- description: "Propose a bounded follow-up in an existing durable private session. This posts a user-visible approval card and performs no target work until explicit approval.",
93
+ description: "Propose reviewed instructions for an exact existing private Driver session, including an update to your active assignment. Busy Drivers queue approved instructions for their next turn; stopped sessions require explicit owner resumption. No target work occurs before human approval.",
94
94
  inputSchema: shape({
95
95
  projectSlug: field("string", "Exact owned target project slug."),
96
96
  targetSessionRef: field("string", "Durable opaque session reference returned by list_project_sessions."),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "5.3.0-beta.4",
3
+ "version": "5.3.0-beta.6",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js"
@@ -1 +0,0 @@
1
- import{l as a,m as b,n as c,o as d}from"./chunk-IZ2ZANEY.js";import"./chunk-7H36SUZA.js";import"./chunk-DGOFGEUK.js";export{a as closeLinear,c as handleLinearResult,d as initLinearPanel,b as openLinear};
@@ -1 +0,0 @@
1
- import{K as a,L as b,M as c,N as d,O as e,P as f}from"./chunk-7H36SUZA.js";import"./chunk-DGOFGEUK.js";export{e as handleSharedBrowserMessage,d as hideSharedBrowser,f as initSharedBrowser,c as openSharedBrowser,b as openSharedBrowserAt,a as sendBrowserInput};