clay-server 3.8.1 → 4.0.0-beta.2
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.
- package/lib/ask-user-mcp-server.js +5 -119
- package/lib/builtin-mates.js +6 -6
- package/lib/claude-hook-installer.js +15 -0
- package/lib/clay-history-mcp-server.js +62 -297
- package/lib/daemon.js +1 -1
- package/lib/debate-model-selection.js +119 -0
- package/lib/home-debate-tool-policy.js +164 -0
- package/lib/home-response-notifications.js +26 -0
- package/lib/mate-creation-mcp-server.js +55 -0
- package/lib/mate-ready-creation.js +95 -0
- package/lib/mates.js +2 -0
- package/lib/notes.js +2 -1
- package/lib/project-ask-user.js +64 -0
- package/lib/project-capsule-catalog.js +101 -0
- package/lib/project-connection.js +4 -0
- package/lib/project-debate-proposal.js +133 -17
- package/lib/project-debate.js +214 -110
- package/lib/project-delegated-follow-up.js +84 -0
- package/lib/project-delegated-session.js +53 -0
- package/lib/project-filesystem.js +7 -9
- package/lib/project-knowledge.js +1 -0
- package/lib/project-mate-creation-proposal.js +120 -0
- package/lib/project-memory.js +14 -7
- package/lib/project-models.js +157 -32
- package/lib/project-session-notes.js +2 -2
- package/lib/project-session-pair.js +8 -8
- package/lib/project-sessions.js +66 -51
- package/lib/project-vendor-login.js +397 -0
- package/lib/project-worker-proposal.js +25 -25
- package/lib/project-workspace-query.js +144 -0
- package/lib/project.js +349 -102
- package/lib/public/app.js +85 -42
- package/lib/public/css/avatar-imprints.css +0 -1
- package/lib/public/css/base.css +4 -0
- package/lib/public/css/capsule-ui.css +389 -0
- package/lib/public/css/command-palette.css +306 -6
- package/lib/public/css/home-capsule-library.css +243 -0
- package/lib/public/css/home-capsule-source.css +192 -0
- package/lib/public/css/home-debate-live.css +146 -0
- package/lib/public/css/home-debate-planning.css +125 -0
- package/lib/public/css/home-debates-archive.css +281 -0
- package/lib/public/css/home-hub.css +896 -405
- package/lib/public/css/home-mate-creation.css +34 -0
- package/lib/public/css/home-mate-model-picker.css +131 -0
- package/lib/public/css/home-mate-settings.css +344 -0
- package/lib/public/css/home-session-actions.css +265 -0
- package/lib/public/css/home-sidebar.css +667 -0
- package/lib/public/css/icon-strip.css +1 -2
- package/lib/public/css/mates.css +7 -34
- package/lib/public/css/pane.css +1 -1
- package/lib/public/css/playbook.css +0 -80
- package/lib/public/css/sticky-notes.css +53 -117
- package/lib/public/css/tui-attention.css +0 -44
- package/lib/public/css/workspace-assignment.css +45 -0
- package/lib/public/fonts/source-serif-4/LICENSE.md +93 -0
- package/lib/public/fonts/source-serif-4/SourceSerif4Caption-Semibold.ttf.woff2 +0 -0
- package/lib/public/index.html +93 -74
- package/lib/public/modules/app-connection.js +10 -1
- package/lib/public/modules/app-dm.js +43 -160
- package/lib/public/modules/app-home-hub.js +328 -585
- package/lib/public/modules/app-message-cards.js +242 -0
- package/lib/public/modules/app-message-router.js +84 -0
- package/lib/public/modules/app-messages.js +102 -37
- package/lib/public/modules/app-notifications.js +8 -119
- package/lib/public/modules/app-projects.js +9 -1
- package/lib/public/modules/app-rendering.js +21 -602
- package/lib/public/modules/avatar-imprint.js +11 -8
- package/lib/public/modules/chat-bubble-renderer.js +152 -0
- package/lib/public/modules/chat-render-runtime.js +198 -0
- package/lib/public/modules/command-palette.js +225 -436
- package/lib/public/modules/dm-render.js +82 -0
- package/lib/public/modules/home-capsule-creation-intent.js +31 -0
- package/lib/public/modules/home-capsule-library.js +146 -0
- package/lib/public/modules/home-capsule-source.js +383 -0
- package/lib/public/modules/home-chat-identity.js +35 -0
- package/lib/public/modules/home-chat-scroll.js +77 -0
- package/lib/public/modules/home-chat-stream-state.js +34 -0
- package/lib/public/modules/home-composer-focus.js +25 -0
- package/lib/public/modules/home-conversations-sheet.js +179 -0
- package/lib/public/modules/home-debate-activity.js +48 -0
- package/lib/public/modules/home-debate-controls.js +295 -0
- package/lib/public/modules/home-debate-launch.js +41 -0
- package/lib/public/modules/home-debate-live.js +284 -0
- package/lib/public/modules/home-debate-models.js +73 -0
- package/lib/public/modules/home-debate-planning.js +335 -0
- package/lib/public/modules/home-debates-archive.js +279 -0
- package/lib/public/modules/home-dock-resize.js +74 -0
- package/lib/public/modules/home-dock.js +446 -0
- package/lib/public/modules/home-mate-chat.js +496 -0
- package/lib/public/modules/home-mate-creation.js +109 -0
- package/lib/public/modules/home-mate-model-picker.js +281 -0
- package/lib/public/modules/home-mate-selection.js +38 -0
- package/lib/public/modules/home-mate-settings-menu.js +131 -0
- package/lib/public/modules/home-mate-settings.js +383 -0
- package/lib/public/modules/home-session-actions.js +373 -0
- package/lib/public/modules/home-session-model-confirmation.js +13 -0
- package/lib/public/modules/home-shell.js +9 -0
- package/lib/public/modules/home-sidebar-chat-list.js +63 -0
- package/lib/public/modules/home-sidebar.js +158 -0
- package/lib/public/modules/home-sub-surface.js +19 -0
- package/lib/public/modules/home-surface-boot.js +68 -0
- package/lib/public/modules/home-surface.js +141 -0
- package/lib/public/modules/home-tools.js +271 -0
- package/lib/public/modules/markdown.js +50 -2
- package/lib/public/modules/mate-management.js +86 -0
- package/lib/public/modules/mate-wizard.js +0 -7
- package/lib/public/modules/mention.js +17 -19
- package/lib/public/modules/pane-bridge.js +9 -0
- package/lib/public/modules/profile.js +4 -5
- package/lib/public/modules/project-settings.js +34 -28
- package/lib/public/modules/project-switcher.js +11 -5
- package/lib/public/modules/project-workspace-assignment.js +23 -0
- package/lib/public/modules/search-clay-chat.js +402 -0
- package/lib/public/modules/server-settings.js +14 -12
- package/lib/public/modules/session-actions.js +1 -1
- package/lib/public/modules/sidebar-mates.js +56 -552
- package/lib/public/modules/sidebar-mobile.js +7 -64
- package/lib/public/modules/sidebar-presence.js +37 -0
- package/lib/public/modules/split-pair-ui.js +13 -13
- package/lib/public/modules/split-view.js +7 -0
- package/lib/public/modules/sticky-notes.js +7 -6
- package/lib/public/modules/tool-input-composition.js +48 -0
- package/lib/public/modules/tool-llm-status.js +167 -0
- package/lib/public/modules/tool-renderer-advanced.js +285 -0
- package/lib/public/modules/tool-renderer-chart.js +158 -0
- package/lib/public/modules/tool-renderer-semantics.js +35 -0
- package/lib/public/modules/tool-renderer.js +464 -0
- package/lib/public/modules/tool-runtime.js +309 -0
- package/lib/public/modules/tool-ui-evaluator.js +97 -0
- package/lib/public/modules/tool-ui-tree.js +21 -0
- package/lib/public/modules/tui-attention.js +10 -0
- package/lib/public/modules/user-settings.js +16 -23
- package/lib/public/modules/vendor-login.js +287 -0
- package/lib/public/modules/worker-proposal.js +2 -2
- package/lib/public/modules/workspace-assignment-card.js +149 -0
- package/lib/public/modules/workspace-assignment-routing.js +4 -0
- package/lib/public/style.css +13 -2
- package/lib/runtime-env.js +65 -0
- package/lib/sdk-bridge.js +120 -32
- package/lib/sdk-message-processor.js +10 -6
- package/lib/server-home-capsule-creation.js +36 -0
- package/lib/server-home-chat-events.js +287 -0
- package/lib/server-home-chat.js +518 -0
- package/lib/server-home-clay-entry.js +65 -0
- package/lib/server-home-clay-session-links.js +26 -0
- package/lib/server-home-debate-planning.js +214 -0
- package/lib/server-home-debates.js +137 -0
- package/lib/server-home-mate-creation.js +261 -0
- package/lib/server-home-models.js +241 -0
- package/lib/server-home-preferences.js +61 -0
- package/lib/server-mates.js +37 -0
- package/lib/server-tools.js +377 -0
- package/lib/server.js +111 -59
- package/lib/session-pair-mcp-server.js +6 -6
- package/lib/sessions.js +18 -3
- package/lib/tool-capsule-source.js +142 -0
- package/lib/tool-control-mcp-server.js +126 -0
- package/lib/tool-llm.js +48 -0
- package/lib/tool-storage.js +77 -0
- package/lib/tool-ui-spec-advanced.js +82 -0
- package/lib/tool-ui-spec.js +261 -0
- package/lib/tools-registry.js +286 -0
- package/lib/users-home-dock-preferences.js +77 -0
- package/lib/users-home-surface-preferences.js +99 -0
- package/lib/users-preferences.js +4 -7
- package/lib/users.js +12 -0
- package/lib/whats-new-content.js +3 -3
- package/lib/workspace-assignment-service.js +420 -0
- package/lib/workspace-query-mcp-server.js +154 -0
- package/lib/workspace-query-service.js +492 -0
- package/lib/ws-schema.js +93 -12
- package/lib/yoke/adapters/acp.js +2 -1
- package/lib/yoke/adapters/antigravity.js +2 -1
- package/lib/yoke/adapters/claude-worker.js +11 -4
- package/lib/yoke/adapters/claude.js +73 -43
- package/lib/yoke/adapters/codex.js +40 -10
- package/lib/yoke/adapters/kiro.js +2 -1
- package/lib/yoke/claude-user-input.js +38 -0
- package/lib/yoke/codex-app-server.js +20 -2
- package/lib/yoke/codex-user-input.js +64 -0
- package/lib/yoke/complete-once.js +164 -0
- package/lib/yoke/index.js +5 -1
- package/lib/yoke/interface.js +19 -0
- package/lib/yoke/mcp-bridge-server.js +2 -6
- package/lib/yoke/user-input.js +254 -0
- package/package.json +3 -2
- package/lib/public/css/home-chat.css +0 -380
- package/lib/public/modules/home-chat.js +0 -438
- package/lib/server-clay-home.js +0 -245
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Per-user persistence for durable Home conversation selections.
|
|
2
|
+
|
|
3
|
+
function attachHomeSurfacePreferences(deps) {
|
|
4
|
+
var loadUsers = deps.loadUsers;
|
|
5
|
+
var saveUsers = deps.saveUsers;
|
|
6
|
+
|
|
7
|
+
function defaults() {
|
|
8
|
+
return { surface: null, projectSlug: null, activeMateId: null, activeSessionByMate: {}, sidebarCollapsed: false, chatScope: "all", subSurface: "chat" };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function validId(value) {
|
|
12
|
+
return typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function validSessionReference(value) {
|
|
16
|
+
return typeof value === "string" && value.length > 0 && value.length <= 256 && !/[\x00-\x1f]/.test(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function validProjectSlug(value) {
|
|
20
|
+
return typeof value === "string" && /^[a-z0-9][a-z0-9_-]{0,127}$/.test(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalize(value) {
|
|
24
|
+
var source = value && typeof value === "object" ? value : {};
|
|
25
|
+
var sessions = source.activeSessionByMate && typeof source.activeSessionByMate === "object" && !Array.isArray(source.activeSessionByMate)
|
|
26
|
+
? source.activeSessionByMate
|
|
27
|
+
: {};
|
|
28
|
+
var normalizedSessions = {};
|
|
29
|
+
var mateIds = Object.keys(sessions);
|
|
30
|
+
for (var i = 0; i < mateIds.length; i++) {
|
|
31
|
+
if (!validId(mateIds[i]) || !validSessionReference(sessions[mateIds[i]])) continue;
|
|
32
|
+
normalizedSessions[mateIds[i]] = sessions[mateIds[i]];
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
surface: source.surface === "home" || source.surface === "project" ? source.surface : null,
|
|
36
|
+
projectSlug: validProjectSlug(source.projectSlug) ? source.projectSlug : null,
|
|
37
|
+
activeMateId: validId(source.activeMateId) ? source.activeMateId : null,
|
|
38
|
+
activeSessionByMate: normalizedSessions,
|
|
39
|
+
sidebarCollapsed: source.sidebarCollapsed === true,
|
|
40
|
+
chatScope: source.chatScope === "current" ? "current" : "all",
|
|
41
|
+
subSurface: source.subSurface === "debates" ? "debates" : "chat",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function loadSingleUserPreference() {
|
|
46
|
+
try {
|
|
47
|
+
var config = require("./config");
|
|
48
|
+
var current = config.loadConfig() || {};
|
|
49
|
+
return normalize(current.homeSurfacePreference);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return defaults();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function saveSingleUserPreference(preference) {
|
|
56
|
+
try {
|
|
57
|
+
var config = require("./config");
|
|
58
|
+
var current = config.loadConfig() || {};
|
|
59
|
+
config.saveConfig(Object.assign({}, current, { homeSurfacePreference: preference }));
|
|
60
|
+
return { ok: true, preference: preference };
|
|
61
|
+
} catch (e) {
|
|
62
|
+
return { error: "Unable to save Home surface preference" };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getHomeSurfacePreference(userId) {
|
|
67
|
+
var data = loadUsers();
|
|
68
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
69
|
+
if (data.users[i].id === userId) return normalize(data.users[i].homeSurfacePreference);
|
|
70
|
+
}
|
|
71
|
+
if (userId === "default") return loadSingleUserPreference();
|
|
72
|
+
return defaults();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function setHomeSurfacePreference(userId, patch) {
|
|
76
|
+
var current = getHomeSurfacePreference(userId);
|
|
77
|
+
var merged = Object.assign({}, current, patch || {});
|
|
78
|
+
if (patch && patch.activeSessionByMate && typeof patch.activeSessionByMate === "object") {
|
|
79
|
+
merged.activeSessionByMate = Object.assign({}, current.activeSessionByMate, patch.activeSessionByMate);
|
|
80
|
+
}
|
|
81
|
+
var next = normalize(merged);
|
|
82
|
+
var data = loadUsers();
|
|
83
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
84
|
+
if (data.users[i].id !== userId) continue;
|
|
85
|
+
data.users[i].homeSurfacePreference = next;
|
|
86
|
+
saveUsers(data);
|
|
87
|
+
return { ok: true, preference: next };
|
|
88
|
+
}
|
|
89
|
+
if (userId === "default") return saveSingleUserPreference(next);
|
|
90
|
+
return { error: "User not found" };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
getHomeSurfacePreference: getHomeSurfacePreference,
|
|
95
|
+
setHomeSurfacePreference: setHomeSurfacePreference,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { attachHomeSurfacePreferences: attachHomeSurfacePreferences };
|
package/lib/users-preferences.js
CHANGED
|
@@ -334,14 +334,11 @@ function attachPreferences(deps) {
|
|
|
334
334
|
return { error: "User not found" };
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
// --- Per-user
|
|
337
|
+
// --- Per-user project Mate DM shortcut toggle ---
|
|
338
338
|
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
// opt-in, not opt-out. Stored as `matesEnabled`; only the literal `true`
|
|
343
|
-
// enables it, so brand-new users (no field set) get the default-off
|
|
344
|
-
// experience and must turn Mates on explicitly in settings.
|
|
339
|
+
// `matesEnabled` is retained as the persisted field for compatibility.
|
|
340
|
+
// It now controls only project-local Mate DM shortcuts. The broader Mate
|
|
341
|
+
// capability surface is always available. Default remains off.
|
|
345
342
|
|
|
346
343
|
function getMatesEnabled(userId) {
|
|
347
344
|
var data = loadUsers();
|
package/lib/users.js
CHANGED
|
@@ -6,6 +6,8 @@ var { CONFIG_DIR } = require("./config");
|
|
|
6
6
|
var { attachAuth } = require("./users-auth");
|
|
7
7
|
var { DEFAULT_PERMISSIONS, ALL_PERMISSIONS, attachPermissions } = require("./users-permissions");
|
|
8
8
|
var { attachPreferences } = require("./users-preferences");
|
|
9
|
+
var { attachHomeDockPreferences } = require("./users-home-dock-preferences");
|
|
10
|
+
var { attachHomeSurfacePreferences } = require("./users-home-surface-preferences");
|
|
9
11
|
|
|
10
12
|
var USERS_FILE = path.join(CONFIG_DIR, "users.json");
|
|
11
13
|
|
|
@@ -394,6 +396,8 @@ function removeExpiredInvites() {
|
|
|
394
396
|
var auth = attachAuth({ loadUsers: loadUsers, saveUsers: saveUsers, findAdmin: findAdmin });
|
|
395
397
|
var permissions = attachPermissions({ loadUsers: loadUsers, saveUsers: saveUsers, findUserById: findUserById });
|
|
396
398
|
var preferences = attachPreferences({ loadUsers: loadUsers, saveUsers: saveUsers });
|
|
399
|
+
var homeDockPreferences = attachHomeDockPreferences({ loadUsers: loadUsers, saveUsers: saveUsers });
|
|
400
|
+
var homeSurfacePreferences = attachHomeSurfacePreferences({ loadUsers: loadUsers, saveUsers: saveUsers });
|
|
397
401
|
|
|
398
402
|
// Alias auth functions
|
|
399
403
|
var isMultiUser = auth.isMultiUser;
|
|
@@ -442,6 +446,10 @@ var markWhatsNewSeen = preferences.markWhatsNewSeen;
|
|
|
442
446
|
var getTerminalFont = preferences.getTerminalFont;
|
|
443
447
|
var setTerminalFont = preferences.setTerminalFont;
|
|
444
448
|
var setMateOnboarded = preferences.setMateOnboarded;
|
|
449
|
+
var getHomeDockPreference = homeDockPreferences.getHomeDockPreference;
|
|
450
|
+
var setHomeDockPreference = homeDockPreferences.setHomeDockPreference;
|
|
451
|
+
var getHomeSurfacePreference = homeSurfacePreferences.getHomeSurfacePreference;
|
|
452
|
+
var setHomeSurfacePreference = homeSurfacePreferences.setHomeSurfacePreference;
|
|
445
453
|
|
|
446
454
|
module.exports = {
|
|
447
455
|
USERS_FILE: USERS_FILE,
|
|
@@ -511,6 +519,10 @@ module.exports = {
|
|
|
511
519
|
markWhatsNewSeen: markWhatsNewSeen,
|
|
512
520
|
getTerminalFont: getTerminalFont,
|
|
513
521
|
setTerminalFont: setTerminalFont,
|
|
522
|
+
getHomeDockPreference: getHomeDockPreference,
|
|
523
|
+
setHomeDockPreference: setHomeDockPreference,
|
|
524
|
+
getHomeSurfacePreference: getHomeSurfacePreference,
|
|
525
|
+
setHomeSurfacePreference: setHomeSurfacePreference,
|
|
514
526
|
getDeletedBuiltinKeys: getDeletedBuiltinKeys,
|
|
515
527
|
addDeletedBuiltinKey: addDeletedBuiltinKey,
|
|
516
528
|
removeDeletedBuiltinKey: removeDeletedBuiltinKey,
|
package/lib/whats-new-content.js
CHANGED
|
@@ -70,13 +70,13 @@ var ENTRIES = [
|
|
|
70
70
|
title: "GUI is back as the default",
|
|
71
71
|
publishedAt: "2026-07-01",
|
|
72
72
|
image: null,
|
|
73
|
-
summary: "Anthropic reversed the June 15 billing split, so SDK usage is no longer billed separately. Clay opens Claude sessions in the GUI chat again by default.
|
|
73
|
+
summary: "Anthropic reversed the June 15 billing split, so SDK usage is no longer billed separately. Clay opens Claude sessions in the GUI chat again by default. Project Mate DM shortcuts are hidden by default.",
|
|
74
74
|
body: '' +
|
|
75
75
|
'<p><strong>Good news: Anthropic walked back the June 15 billing change.</strong> The planned Interactive vs. Programmatic split (which would have charged Claude Agent SDK usage at full API rates) is off the table, so the reason we moved everyone to the TUI is gone.</p>' +
|
|
76
76
|
'<h3>GUI is the default again</h3>' +
|
|
77
77
|
'<p>New Claude sessions open in the <strong>GUI chat</strong> (SDK) by default, the way they did before. The embedded <code>claude</code> <strong>TUI</strong> is still there whenever you want it — toggle <strong>"Open Claude as terminal (TUI)"</strong> in Settings.</p>' +
|
|
78
|
-
'<h3>
|
|
79
|
-
'<p>
|
|
78
|
+
'<h3>Project Mate DM shortcuts are optional</h3>' +
|
|
79
|
+
'<p>Mate DM shortcuts inside project workspaces are hidden by default and can be shown in <strong>Settings</strong>. Mates themselves remain available in Clay Home, together with their conversations, memory, debates, and project collaboration.</p>' +
|
|
80
80
|
'<p>Clay only gets sharper from here.</p>',
|
|
81
81
|
},
|
|
82
82
|
];
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
var fs = require("fs");
|
|
2
|
+
var path = require("path");
|
|
3
|
+
var crypto = require("crypto");
|
|
4
|
+
|
|
5
|
+
var ACTIVE = { proposed: true, starting: true, running: true };
|
|
6
|
+
var MAX_RUNNING = 3;
|
|
7
|
+
|
|
8
|
+
function clean(value, max) {
|
|
9
|
+
if (typeof value !== "string") return "";
|
|
10
|
+
return value.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim().slice(0, max);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function userStorageKey(userId) {
|
|
14
|
+
return crypto.createHash("sha256").update(String(userId || "_single_user")).digest("hex").slice(0, 24) + ".json";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function attachWorkspaceAssignmentService(ctx) {
|
|
18
|
+
var storageDir = ctx.storageDir || null;
|
|
19
|
+
var records = [];
|
|
20
|
+
var unsubscribers = {};
|
|
21
|
+
var pendingRestartReplay = {};
|
|
22
|
+
|
|
23
|
+
function recordFile(userId) {
|
|
24
|
+
return storageDir ? path.join(storageDir, userStorageKey(userId)) : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function saveUser(userId) {
|
|
28
|
+
var file = recordFile(userId);
|
|
29
|
+
if (!file) return;
|
|
30
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
31
|
+
var owned = records.filter(function (record) { return record.userId === (userId || null); });
|
|
32
|
+
var temp = file + ".tmp." + process.pid;
|
|
33
|
+
fs.writeFileSync(temp, JSON.stringify(owned, null, 2), { mode: 0o600 });
|
|
34
|
+
fs.renameSync(temp, file);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function load() {
|
|
38
|
+
if (!storageDir) return;
|
|
39
|
+
var files = [];
|
|
40
|
+
try { files = fs.readdirSync(storageDir); } catch (e) { return; }
|
|
41
|
+
for (var i = 0; i < files.length; i++) {
|
|
42
|
+
if (!files[i].endsWith(".json") || files[i].indexOf(".tmp.") !== -1) continue;
|
|
43
|
+
try {
|
|
44
|
+
var loaded = JSON.parse(fs.readFileSync(path.join(storageDir, files[i]), "utf8"));
|
|
45
|
+
if (Array.isArray(loaded)) records = records.concat(loaded);
|
|
46
|
+
} catch (e) {}
|
|
47
|
+
}
|
|
48
|
+
var changedUsers = {};
|
|
49
|
+
for (var j = 0; j < records.length; j++) {
|
|
50
|
+
if (records[j].status !== "starting" && records[j].status !== "running") continue;
|
|
51
|
+
records[j].status = "interrupted";
|
|
52
|
+
records[j].error = "Clay restarted before this assignment completed.";
|
|
53
|
+
records[j].updatedAt = Date.now();
|
|
54
|
+
pendingRestartReplay[records[j].assignmentId] = true;
|
|
55
|
+
changedUsers[String(records[j].userId || "_single_user")] = records[j].userId || null;
|
|
56
|
+
}
|
|
57
|
+
var keys = Object.keys(changedUsers);
|
|
58
|
+
for (var k = 0; k < keys.length; k++) saveUser(changedUsers[keys[k]]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function sourceKey(principal) {
|
|
62
|
+
return String(principal.userId || "_single_user") + ":" + principal.sourceProjectSlug + ":" + principal.sourceSessionRef;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizePrincipal(principal) {
|
|
66
|
+
if (!principal || !principal.sourceProjectSlug) return null;
|
|
67
|
+
var project = ctx.getProjects().get(principal.sourceProjectSlug);
|
|
68
|
+
var manager = project && project.getSessionManager ? project.getSessionManager() : null;
|
|
69
|
+
var session = manager && manager.sessions ? manager.sessions.get(principal.sourceSessionId) : null;
|
|
70
|
+
if (!session && manager && manager.sessions && principal.sourceSessionRef) {
|
|
71
|
+
manager.sessions.forEach(function (candidate) {
|
|
72
|
+
if (!session && ctx.sessionRef(principal.sourceProjectSlug, candidate) === principal.sourceSessionRef) session = candidate;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (!session) return null;
|
|
76
|
+
return Object.assign({}, principal, {
|
|
77
|
+
sourceSessionId: session.localId,
|
|
78
|
+
sourceSessionRef: ctx.sessionRef(principal.sourceProjectSlug, session),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function publicRecord(record) {
|
|
83
|
+
return {
|
|
84
|
+
assignmentId: record.assignmentId,
|
|
85
|
+
delivery: record.delivery || "new_session",
|
|
86
|
+
projectSlug: record.targetProjectSlug,
|
|
87
|
+
projectTitle: record.targetProjectTitle,
|
|
88
|
+
title: record.title,
|
|
89
|
+
task: record.task,
|
|
90
|
+
status: record.status,
|
|
91
|
+
sourceProjectSlug: record.sourceProjectSlug,
|
|
92
|
+
sourceSessionRef: record.sourceSessionRef,
|
|
93
|
+
targetSessionRef: record.targetSessionRef || null,
|
|
94
|
+
targetSessionTitle: record.targetSessionTitle || null,
|
|
95
|
+
resultSummary: record.resultSummary || "",
|
|
96
|
+
error: record.error || "",
|
|
97
|
+
createdAt: record.createdAt,
|
|
98
|
+
updatedAt: record.updatedAt,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function ownsProject(record, status) {
|
|
103
|
+
if (!status || status.isWorktree || status.isMate) return false;
|
|
104
|
+
if (ctx.isMultiUser()) return !!record.userId && status.projectOwnerId === record.userId;
|
|
105
|
+
return !status.projectOwnerId || (!!record.singleUserOwnerId && status.projectOwnerId === record.singleUserOwnerId);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function exactTarget(record) {
|
|
109
|
+
var project = ctx.getProjects().get(record.targetProjectSlug);
|
|
110
|
+
if (!project || !ownsProject(record, project.getStatus())) return null;
|
|
111
|
+
return project;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function exactSource(record) {
|
|
115
|
+
var project = ctx.getProjects().get(record.sourceProjectSlug);
|
|
116
|
+
if (!project) return null;
|
|
117
|
+
var status = project.getStatus();
|
|
118
|
+
if (!status || status.isMate !== true || status.mateId !== record.sourceMateId) return null;
|
|
119
|
+
if (ctx.isMultiUser() && status.projectOwnerId !== record.userId) return null;
|
|
120
|
+
var manager = project.getSessionManager();
|
|
121
|
+
var session = null;
|
|
122
|
+
if (manager && manager.sessions) {
|
|
123
|
+
manager.sessions.forEach(function (candidate) {
|
|
124
|
+
if (!session && ctx.sessionRef(record.sourceProjectSlug, candidate) === record.sourceSessionRef) session = candidate;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (!session) return null;
|
|
128
|
+
if (ctx.isMultiUser() && session.ownerId !== record.userId) return null;
|
|
129
|
+
if (!ctx.isMultiUser() && session.ownerId && session.ownerId !== record.singleUserOwnerId) return null;
|
|
130
|
+
return { project: project, manager: manager, session: session };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function emit(record, eventType) {
|
|
134
|
+
var source = exactSource(record);
|
|
135
|
+
if (!source) return;
|
|
136
|
+
source.manager.sendAndRecord(source.session, {
|
|
137
|
+
type: eventType || "project_assignment_status",
|
|
138
|
+
assignment: publicRecord(record),
|
|
139
|
+
requestId: record.sourceRequestId || null,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function reconcileProject(projectSlug) {
|
|
144
|
+
for (var i = 0; i < records.length; i++) {
|
|
145
|
+
if (!pendingRestartReplay[records[i].assignmentId] || records[i].sourceProjectSlug !== projectSlug) continue;
|
|
146
|
+
if (!exactSource(records[i])) continue;
|
|
147
|
+
emit(records[i]);
|
|
148
|
+
delete pendingRestartReplay[records[i].assignmentId];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function propose(principal, args) {
|
|
153
|
+
principal = normalizePrincipal(principal);
|
|
154
|
+
if (!principal || !principal.sourceSessionRef) throw new Error("Project assignments require an exact session-bound Mate query.");
|
|
155
|
+
var projectSlug = clean(args.projectSlug, 120);
|
|
156
|
+
var title = clean(args.title, 160);
|
|
157
|
+
var task = clean(args.task, 12000);
|
|
158
|
+
if (!projectSlug || !title || !task) throw new Error("projectSlug, title, and task are required.");
|
|
159
|
+
var candidate = {
|
|
160
|
+
userId: principal.userId || null,
|
|
161
|
+
singleUserOwnerId: principal.singleUserOwnerId || null,
|
|
162
|
+
targetProjectSlug: projectSlug,
|
|
163
|
+
};
|
|
164
|
+
var target = exactTarget(candidate);
|
|
165
|
+
if (!target) throw new Error("Target project not found in the owned workspace.");
|
|
166
|
+
var key = sourceKey(principal);
|
|
167
|
+
var idempotency = crypto.createHash("sha256").update(key + "\u0000" + projectSlug + "\u0000" + title + "\u0000" + task).digest("hex");
|
|
168
|
+
for (var i = 0; i < records.length; i++) {
|
|
169
|
+
if (records[i].idempotencyKey === idempotency) return publicRecord(records[i]);
|
|
170
|
+
if (records[i].sourceKey === key && ACTIVE[records[i].status]) throw new Error("This conversation already has an active assignment.");
|
|
171
|
+
}
|
|
172
|
+
var now = Date.now();
|
|
173
|
+
var targetStatus = target.getStatus();
|
|
174
|
+
var record = {
|
|
175
|
+
assignmentId: "assignment_" + crypto.randomUUID(),
|
|
176
|
+
idempotencyKey: idempotency,
|
|
177
|
+
sourceKey: key,
|
|
178
|
+
userId: principal.userId || null,
|
|
179
|
+
singleUserOwnerId: principal.singleUserOwnerId || null,
|
|
180
|
+
sourceMateId: principal.mateId,
|
|
181
|
+
sourceProjectSlug: principal.sourceProjectSlug,
|
|
182
|
+
sourceSessionRef: principal.sourceSessionRef,
|
|
183
|
+
sourceRequestId: principal.sourceRequestId || null,
|
|
184
|
+
targetProjectSlug: projectSlug,
|
|
185
|
+
targetProjectTitle: clean(targetStatus.title || targetStatus.project || projectSlug, 160),
|
|
186
|
+
title: title,
|
|
187
|
+
task: task,
|
|
188
|
+
delivery: "new_session",
|
|
189
|
+
status: "proposed",
|
|
190
|
+
createdAt: now,
|
|
191
|
+
updatedAt: now,
|
|
192
|
+
};
|
|
193
|
+
if (!exactSource(record)) throw new Error("Source conversation is no longer available.");
|
|
194
|
+
records.push(record);
|
|
195
|
+
saveUser(record.userId);
|
|
196
|
+
emit(record, "project_assignment_proposal");
|
|
197
|
+
return publicRecord(record);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function proposeFollowUp(principal, args) {
|
|
201
|
+
principal = normalizePrincipal(principal);
|
|
202
|
+
if (!principal || !principal.sourceSessionRef) throw new Error("Project follow-ups require an exact session-bound Mate query.");
|
|
203
|
+
var projectSlug = clean(args.projectSlug, 120);
|
|
204
|
+
var targetSessionRef = clean(args.targetSessionRef, 100);
|
|
205
|
+
var title = clean(args.title, 160);
|
|
206
|
+
var task = clean(args.task, 12000);
|
|
207
|
+
if (!projectSlug || !targetSessionRef || !title || !task) throw new Error("projectSlug, targetSessionRef, title, and task are required.");
|
|
208
|
+
var key = sourceKey(principal);
|
|
209
|
+
var idempotency = crypto.createHash("sha256").update(key + "\u0000follow_up\u0000" + projectSlug + "\u0000" + targetSessionRef + "\u0000" + title + "\u0000" + task).digest("hex");
|
|
210
|
+
for (var i = 0; i < records.length; i++) {
|
|
211
|
+
if (records[i].idempotencyKey === idempotency) return publicRecord(records[i]);
|
|
212
|
+
if (records[i].sourceKey === key && ACTIVE[records[i].status]) throw new Error("This conversation already has an active assignment.");
|
|
213
|
+
}
|
|
214
|
+
var candidate = { userId: principal.userId || null, singleUserOwnerId: principal.singleUserOwnerId || null, targetProjectSlug: projectSlug };
|
|
215
|
+
var target = exactTarget(candidate);
|
|
216
|
+
if (!target || typeof target.inspectDelegatedFollowUp !== "function") throw new Error("Target project is unavailable for follow-ups.");
|
|
217
|
+
var inspected = target.inspectDelegatedFollowUp({ userId: principal.userId }, targetSessionRef);
|
|
218
|
+
var now = Date.now();
|
|
219
|
+
var targetStatus = target.getStatus();
|
|
220
|
+
var record = {
|
|
221
|
+
assignmentId: "assignment_" + crypto.randomUUID(), idempotencyKey: idempotency, sourceKey: key,
|
|
222
|
+
userId: principal.userId || null, singleUserOwnerId: principal.singleUserOwnerId || null,
|
|
223
|
+
sourceMateId: principal.mateId, sourceProjectSlug: principal.sourceProjectSlug,
|
|
224
|
+
sourceSessionRef: principal.sourceSessionRef, sourceRequestId: principal.sourceRequestId || null,
|
|
225
|
+
targetProjectSlug: projectSlug, targetProjectTitle: clean(targetStatus.title || targetStatus.project || projectSlug, 160),
|
|
226
|
+
targetSessionRef: inspected.sessionRef, targetSessionTitle: clean(inspected.title || "Existing session", 160),
|
|
227
|
+
title: title, task: task, delivery: "follow_up", status: "proposed", createdAt: now, updatedAt: now,
|
|
228
|
+
};
|
|
229
|
+
if (!exactSource(record)) throw new Error("Source conversation is no longer available.");
|
|
230
|
+
records.push(record);
|
|
231
|
+
saveUser(record.userId);
|
|
232
|
+
emit(record, "project_assignment_proposal");
|
|
233
|
+
return publicRecord(record);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function findOwned(userId, assignmentId) {
|
|
237
|
+
for (var i = 0; i < records.length; i++) {
|
|
238
|
+
if (records[i].assignmentId === assignmentId && records[i].userId === (userId || null)) return records[i];
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function responseUserId(ws) {
|
|
244
|
+
return ctx.isMultiUser() && ws && ws._clayUser ? ws._clayUser.id : null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function summarize(session) {
|
|
248
|
+
var text = "";
|
|
249
|
+
for (var i = (session && session.history || []).length - 1; i >= 0; i--) {
|
|
250
|
+
if (session.history[i].type === "delta") text = session.history[i].text + text;
|
|
251
|
+
else if (text && (session.history[i].type === "user_message" || session.history[i].type === "delegated_work" || session.history[i].type === "delegated_follow_up")) break;
|
|
252
|
+
}
|
|
253
|
+
return clean(text, 1200);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function settle(record, status, session, error) {
|
|
257
|
+
if (!ACTIVE[record.status]) return publicRecord(record);
|
|
258
|
+
record.status = status;
|
|
259
|
+
record.resultSummary = status === "completed" ? summarize(session) : "";
|
|
260
|
+
record.error = clean(error || "", 800);
|
|
261
|
+
record.updatedAt = Date.now();
|
|
262
|
+
if (unsubscribers[record.assignmentId]) unsubscribers[record.assignmentId]();
|
|
263
|
+
delete unsubscribers[record.assignmentId];
|
|
264
|
+
saveUser(record.userId);
|
|
265
|
+
emit(record);
|
|
266
|
+
return publicRecord(record);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function watch(record, manager, session) {
|
|
270
|
+
var unsubscribe = manager.subscribeSession(session.localId, function (event) {
|
|
271
|
+
if (event.type === "session_id") {
|
|
272
|
+
record.targetSessionRef = ctx.sessionRef(record.targetProjectSlug, session);
|
|
273
|
+
record.updatedAt = Date.now();
|
|
274
|
+
saveUser(record.userId);
|
|
275
|
+
emit(record);
|
|
276
|
+
} else if (event.type === "error") {
|
|
277
|
+
settle(record, "failed", session, event.text || "Assignment failed.");
|
|
278
|
+
} else if (event.type === "done" || event.type === "result") {
|
|
279
|
+
settle(record, session._lastTurnInterrupted ? "interrupted" : "completed", session, "");
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
if (unsubscribe) unsubscribers[record.assignmentId] = unsubscribe;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function approve(record) {
|
|
286
|
+
if (record.status !== "proposed") return publicRecord(record);
|
|
287
|
+
var running = records.filter(function (item) {
|
|
288
|
+
return item.userId === record.userId && (item.status === "starting" || item.status === "running");
|
|
289
|
+
}).length;
|
|
290
|
+
if (running >= MAX_RUNNING) throw new Error("Too many assignments are already running.");
|
|
291
|
+
if (!exactSource(record)) throw new Error("Source conversation is no longer available.");
|
|
292
|
+
var target = exactTarget(record);
|
|
293
|
+
var followUp = record.delivery === "follow_up";
|
|
294
|
+
if (!target || (followUp ? typeof target.dispatchDelegatedFollowUp !== "function" : typeof target.createDelegatedSession !== "function")) throw new Error("Target project is no longer available.");
|
|
295
|
+
record.decision = "approve";
|
|
296
|
+
record.status = "starting";
|
|
297
|
+
record.updatedAt = Date.now();
|
|
298
|
+
saveUser(record.userId);
|
|
299
|
+
emit(record);
|
|
300
|
+
try {
|
|
301
|
+
var metadata = {
|
|
302
|
+
assignmentId: record.assignmentId,
|
|
303
|
+
title: record.title,
|
|
304
|
+
sourceMateId: record.sourceMateId,
|
|
305
|
+
sourceProjectSlug: record.sourceProjectSlug,
|
|
306
|
+
sourceSessionRef: record.sourceSessionRef,
|
|
307
|
+
};
|
|
308
|
+
var session;
|
|
309
|
+
if (followUp) {
|
|
310
|
+
session = await target.dispatchDelegatedFollowUp({ userId: record.userId }, record.targetSessionRef, record.task, metadata, function (existing) { watch(record, target.getSessionManager(), existing); });
|
|
311
|
+
} else {
|
|
312
|
+
session = await target.createDelegatedSession({ userId: record.userId }, record.task, metadata, function (created) { watch(record, target.getSessionManager(), created); });
|
|
313
|
+
}
|
|
314
|
+
if (record.status !== "starting") return publicRecord(record);
|
|
315
|
+
record.targetSessionRef = ctx.sessionRef(record.targetProjectSlug, session);
|
|
316
|
+
record.targetSessionId = session.localId;
|
|
317
|
+
record.status = "running";
|
|
318
|
+
record.updatedAt = Date.now();
|
|
319
|
+
saveUser(record.userId);
|
|
320
|
+
emit(record);
|
|
321
|
+
} catch (e) {
|
|
322
|
+
settle(record, "failed", null, e.message || String(e));
|
|
323
|
+
}
|
|
324
|
+
return publicRecord(record);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function cancel(record) {
|
|
328
|
+
if (record.status !== "proposed") return publicRecord(record);
|
|
329
|
+
record.decision = "cancel";
|
|
330
|
+
record.status = "cancelled";
|
|
331
|
+
record.updatedAt = Date.now();
|
|
332
|
+
saveUser(record.userId);
|
|
333
|
+
emit(record);
|
|
334
|
+
return publicRecord(record);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function getStatus(principal, assignmentId) {
|
|
338
|
+
principal = normalizePrincipal(principal);
|
|
339
|
+
if (!principal) throw new Error("Assignment not found for this conversation.");
|
|
340
|
+
var record = findOwned(principal.userId, assignmentId);
|
|
341
|
+
if (!record || record.sourceKey !== sourceKey(principal)) throw new Error("Assignment not found for this conversation.");
|
|
342
|
+
return publicRecord(record);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function validateResponseSource(ws, msg, record, routedProjectSlug) {
|
|
346
|
+
var source = exactSource(record);
|
|
347
|
+
if (!source) throw new Error("Source conversation is no longer available.");
|
|
348
|
+
var userId = responseUserId(ws);
|
|
349
|
+
if (record.userId !== (userId || null)) throw new Error("Assignment access denied.");
|
|
350
|
+
if (msg.sourceProjectSlug !== record.sourceProjectSlug || msg.sourceSessionRef !== record.sourceSessionRef) {
|
|
351
|
+
throw new Error("Assignment response does not match its source conversation.");
|
|
352
|
+
}
|
|
353
|
+
if (msg.surface === "home") {
|
|
354
|
+
var tap = ws && ws._homeChatTap;
|
|
355
|
+
if (!tap || tap.mateSlug !== record.sourceProjectSlug || tap.sessionId !== source.session.localId) {
|
|
356
|
+
throw new Error("Open the exact Home conversation before responding.");
|
|
357
|
+
}
|
|
358
|
+
if (msg.mateId !== tap.mateId || msg.sessionId !== tap.sessionReference || msg.requestId !== tap.requestId) {
|
|
359
|
+
throw new Error("Assignment response is stale for this Home conversation.");
|
|
360
|
+
}
|
|
361
|
+
} else if (routedProjectSlug !== record.sourceProjectSlug || !ws || ws._clayActiveSession !== source.session.localId) {
|
|
362
|
+
throw new Error("Open the exact project conversation before responding.");
|
|
363
|
+
}
|
|
364
|
+
return source;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function respond(ws, msg, routedProjectSlug) {
|
|
368
|
+
var userId = responseUserId(ws);
|
|
369
|
+
var record = findOwned(userId, clean(msg.assignmentId, 100));
|
|
370
|
+
if (!record) throw new Error("Assignment proposal is stale or unavailable.");
|
|
371
|
+
validateResponseSource(ws, msg, record, routedProjectSlug);
|
|
372
|
+
var action = msg.action === "approve" ? "approve" : (msg.action === "cancel" ? "cancel" : "");
|
|
373
|
+
if (!action) throw new Error("Choose approve or cancel.");
|
|
374
|
+
if (record.status !== "proposed") {
|
|
375
|
+
if (record.decision === action) return publicRecord(record);
|
|
376
|
+
throw new Error("Assignment proposal has already been resolved.");
|
|
377
|
+
}
|
|
378
|
+
return action === "approve" ? approve(record) : cancel(record);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function handleMessage(ws, msg, routedProjectSlug) {
|
|
382
|
+
if (!msg || msg.type !== "project_assignment_response") return false;
|
|
383
|
+
respond(ws, msg, routedProjectSlug).then(function (assignment) {
|
|
384
|
+
if (ws.readyState !== 1) return;
|
|
385
|
+
if (msg.surface === "home") {
|
|
386
|
+
ws.send(JSON.stringify({ type: "home_project_assignment_status", assignment: assignment, mateId: msg.mateId, sessionId: msg.sessionId, requestId: msg.requestId }));
|
|
387
|
+
} else {
|
|
388
|
+
ws.send(JSON.stringify({ type: "project_assignment_status", assignment: assignment }));
|
|
389
|
+
}
|
|
390
|
+
}).catch(function (error) {
|
|
391
|
+
if (ws.readyState !== 1) return;
|
|
392
|
+
if (msg.surface === "home") {
|
|
393
|
+
var userId = responseUserId(ws);
|
|
394
|
+
var record = findOwned(userId, msg.assignmentId);
|
|
395
|
+
var assignment = record ? publicRecord(record) : { assignmentId: msg.assignmentId || null, status: "proposed" };
|
|
396
|
+
assignment.error = error.message || String(error);
|
|
397
|
+
ws.send(JSON.stringify({ type: "home_project_assignment_status", assignment: assignment, mateId: msg.mateId, sessionId: msg.sessionId, requestId: msg.requestId }));
|
|
398
|
+
} else {
|
|
399
|
+
ws.send(JSON.stringify({ type: "project_assignment_error", assignmentId: msg.assignmentId || null, error: error.message || String(error) }));
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
load();
|
|
406
|
+
return {
|
|
407
|
+
propose: propose,
|
|
408
|
+
proposeFollowUp: proposeFollowUp,
|
|
409
|
+
getStatus: getStatus,
|
|
410
|
+
respond: respond,
|
|
411
|
+
handleMessage: handleMessage,
|
|
412
|
+
reconcileProject: reconcileProject,
|
|
413
|
+
records: records,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
module.exports = {
|
|
418
|
+
attachWorkspaceAssignmentService: attachWorkspaceAssignmentService,
|
|
419
|
+
userStorageKey: userStorageKey,
|
|
420
|
+
};
|