clay-server 4.1.0-beta.6 → 4.1.0-beta.7
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/LICENSE +661 -21
- package/LICENSES/MIT-legacy.txt +21 -0
- package/LICENSES/README.md +7 -0
- package/README.md +2 -2
- package/bin/cli.js +0 -2
- package/lib/daemon.js +0 -2
- package/lib/default-ai-runtime.js +117 -0
- package/lib/git-cli.js +1 -0
- package/lib/issue-work-session.js +10 -0
- package/lib/issues-comments.js +40 -0
- package/lib/issues-commit-verification.js +30 -0
- package/lib/issues-mcp-server.js +27 -0
- package/lib/issues-schema.js +119 -0
- package/lib/issues-service.js +339 -0
- package/lib/issues-store.js +210 -0
- package/lib/model-entry.js +15 -0
- package/lib/project-capsule-turn.js +1 -0
- package/lib/project-connection.js +2 -0
- package/lib/project-delegated-session.js +1 -0
- package/lib/project-filesystem.js +10 -10
- package/lib/project-issue-launch.js +154 -0
- package/lib/project-issues.js +58 -0
- package/lib/project-logs-mcp-server.js +8 -12
- package/lib/project-message-delivery.js +4 -2
- package/lib/project-models.js +71 -3
- package/lib/project-pending-message-queue.js +264 -0
- package/lib/project-scheduled-tasks.js +2 -4
- package/lib/project-sessions.js +82 -31
- package/lib/project-tool-llm-runtime.js +48 -0
- package/lib/project-user-message.js +178 -42
- package/lib/project-workspace-query.js +25 -0
- package/lib/project.js +102 -18
- package/lib/public/app.js +38 -4
- package/lib/public/css/default-ai.css +33 -0
- package/lib/public/css/input.css +89 -5
- package/lib/public/css/issues.css +101 -0
- package/lib/public/css/messages.css +19 -0
- package/lib/public/css/pending-message-queue.css +187 -0
- package/lib/public/css/scheduled-tasks.css +3 -4
- package/lib/public/index.html +25 -17
- package/lib/public/modules/app-connection.js +3 -0
- package/lib/public/modules/app-cursors.js +29 -12
- package/lib/public/modules/app-message-router.js +8 -0
- package/lib/public/modules/app-messages.js +12 -3
- package/lib/public/modules/clay-file-links.js +111 -0
- package/lib/public/modules/clay-issue-links.js +131 -0
- package/lib/public/modules/clay-log-links.js +2 -0
- package/lib/public/modules/cursor-sharing-preference.js +38 -0
- package/lib/public/modules/default-ai.js +377 -0
- package/lib/public/modules/filebrowser.js +43 -2
- package/lib/public/modules/input.js +7 -17
- package/lib/public/modules/issues-context.js +117 -0
- package/lib/public/modules/issues-render.js +29 -0
- package/lib/public/modules/issues.js +270 -0
- package/lib/public/modules/markdown.js +4 -0
- package/lib/public/modules/message-delivery.js +5 -3
- package/lib/public/modules/pending-message-image-editor.js +143 -0
- package/lib/public/modules/pending-message-queue-model.js +22 -0
- package/lib/public/modules/pending-message-queue.js +449 -0
- package/lib/public/modules/scheduled-task-engine.js +0 -50
- package/lib/public/modules/scheduled-tasks.js +7 -16
- package/lib/public/modules/sidebar-mobile.js +3 -0
- package/lib/public/modules/sticky-note-markdown.js +1 -0
- package/lib/public/modules/terminal.js +2 -0
- package/lib/public/modules/tool-palette-order.js +1 -0
- package/lib/public/style.css +2 -0
- package/lib/sdk-bridge.js +34 -11
- package/lib/server-cursor-sharing.js +65 -0
- package/lib/server-default-ai.js +265 -0
- package/lib/server-home-chat.js +29 -9
- package/lib/server-home-clay-entry.js +4 -1
- package/lib/server-home-debate-planning.js +32 -14
- package/lib/server-home-mate-creation.js +30 -11
- package/lib/server-home-models.js +14 -6
- package/lib/server-issue-links.js +18 -0
- package/lib/server.js +41 -0
- package/lib/session-notes-mcp-server.js +1 -1
- package/lib/session-search.js +4 -1
- package/lib/tool-llm.js +1 -0
- package/lib/users-default-ai-preferences.js +80 -0
- package/lib/users-preferences.js +35 -0
- package/lib/users.js +10 -0
- package/lib/workspace-query-projection.js +29 -0
- package/lib/workspace-query-service.js +19 -29
- package/lib/ws-schema.js +36 -1
- package/lib/yoke/adapters/claude-worker.js +4 -2
- package/lib/yoke/adapters/claude.js +5 -1
- package/lib/yoke/package.json +1 -1
- package/package.json +5 -6
- package/bin/claude-relay.js +0 -6
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
var fs = require("fs");
|
|
2
|
+
var path = require("path");
|
|
3
|
+
var crypto = require("crypto");
|
|
4
|
+
var config = require("./config");
|
|
5
|
+
var sessionProvenance = require("./session-provenance");
|
|
6
|
+
|
|
7
|
+
var MAX_TEXT = 100000;
|
|
8
|
+
var MAX_PASTES = 20;
|
|
9
|
+
var MAX_PASTE_LENGTH = 500000;
|
|
10
|
+
var MAX_IMAGES = 20;
|
|
11
|
+
var MAX_IMAGE_DATA = 25 * 1024 * 1024;
|
|
12
|
+
var MAX_TOTAL_BYTES = 32 * 1024 * 1024;
|
|
13
|
+
var MAX_ACTIVE_ITEMS = 100;
|
|
14
|
+
var IMAGE_TYPE_RE = /^image\/[a-z0-9.+-]{1,64}$/i;
|
|
15
|
+
var CLIENT_MESSAGE_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
16
|
+
|
|
17
|
+
function defaultFilePath(opts) {
|
|
18
|
+
var identity = String(opts.slug || "") + "\n" + path.resolve(opts.cwd || process.cwd());
|
|
19
|
+
return path.join(config.CONFIG_DIR, "pending-message-queues", crypto.createHash("sha256").update(identity).digest("hex") + ".json");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createPendingMessageQueue(opts) {
|
|
23
|
+
var filePath = opts.filePath || defaultFilePath(opts);
|
|
24
|
+
var broadcast = opts.broadcast || opts.send || function () {};
|
|
25
|
+
var authorize = opts.authorize || function () { return true; };
|
|
26
|
+
var canMutate = opts.canMutate || authorize;
|
|
27
|
+
var loaded = load();
|
|
28
|
+
var data = loaded.data;
|
|
29
|
+
var loadError = loaded.error;
|
|
30
|
+
|
|
31
|
+
function emptyData() { return { version: 1, revision: 0, nextId: 1, items: [], paused: {} }; }
|
|
32
|
+
function validStored(parsed) {
|
|
33
|
+
if (!parsed || parsed.version !== 1 || !Number.isInteger(parsed.revision) || parsed.revision < 0 || !Number.isInteger(parsed.nextId) || parsed.nextId < 1 || !Array.isArray(parsed.items)) return false;
|
|
34
|
+
if (parsed.paused !== undefined && (!parsed.paused || typeof parsed.paused !== "object" || Array.isArray(parsed.paused))) return false;
|
|
35
|
+
var pausedKeys = Object.keys(parsed.paused || {});
|
|
36
|
+
for (var pausedIndex = 0; pausedIndex < pausedKeys.length; pausedIndex++) if (pausedKeys[pausedIndex].length > 300 || parsed.paused[pausedKeys[pausedIndex]] !== true) return false;
|
|
37
|
+
for (var i = 0; i < parsed.items.length; i++) {
|
|
38
|
+
var item = parsed.items[i];
|
|
39
|
+
if (!item || typeof item.id !== "string" || typeof item.sessionKey !== "string" || typeof item.projectSlug !== "string" || typeof item.actorId !== "string" || !item.message || ["pending", "claimed", "consumed", "cancelled", "failed"].indexOf(item.state) < 0) return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
function load() {
|
|
44
|
+
if (!fs.existsSync(filePath)) return { data: emptyData(), error: null };
|
|
45
|
+
try {
|
|
46
|
+
var parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
47
|
+
if (!validStored(parsed)) return { data: emptyData(), error: "Queue storage is malformed" };
|
|
48
|
+
if (!parsed.paused) parsed.paused = {};
|
|
49
|
+
return { data: parsed, error: null };
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return { data: emptyData(), error: "Queue storage is unreadable" };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function save() {
|
|
55
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
56
|
+
var tmp = filePath + ".tmp." + process.pid + "." + crypto.randomBytes(6).toString("hex");
|
|
57
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
|
|
58
|
+
fs.renameSync(tmp, filePath);
|
|
59
|
+
}
|
|
60
|
+
var persist = opts.persist || save;
|
|
61
|
+
function key(session) {
|
|
62
|
+
var hadOrigin = !!(session && session.sessionOriginId);
|
|
63
|
+
var origin = sessionProvenance.ensureOrigin(session);
|
|
64
|
+
if (!hadOrigin && origin && typeof opts.ensureSessionIdentity === "function") opts.ensureSessionIdentity(session);
|
|
65
|
+
return String(origin);
|
|
66
|
+
}
|
|
67
|
+
function actorId(actor) { return actor && actor.id ? String(actor.id) : "default"; }
|
|
68
|
+
function copy(value) { return JSON.parse(JSON.stringify(value)); }
|
|
69
|
+
function restore(snapshot) { data.version = snapshot.version; data.revision = snapshot.revision; data.nextId = snapshot.nextId; data.items = snapshot.items; data.paused = snapshot.paused || {}; }
|
|
70
|
+
function targetKey(session) { return String(opts.slug) + ":" + key(session); }
|
|
71
|
+
function isPaused(session) { return !!(session && data.paused[targetKey(session)]); }
|
|
72
|
+
function sameTarget(item, session) { return !!(item && session && item.sessionKey === key(session) && item.projectSlug === opts.slug); }
|
|
73
|
+
function scoped(item, session, actor) { return sameTarget(item, session) && item.actorId === actorId(actor); }
|
|
74
|
+
function visible(session, actor) {
|
|
75
|
+
if (loadError || !session || !authorize(session, actor)) return [];
|
|
76
|
+
var out = [];
|
|
77
|
+
for (var i = 0; i < data.items.length; i++) if (sameTarget(data.items[i], session)) out.push(copy(data.items[i]));
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
function result(ok, error, session, actor) {
|
|
81
|
+
return { ok: ok, error: error || null, revision: data.revision, sessionId: session ? session.localId : null, projectSlug: opts.slug, paused: session ? isPaused(session) : false, items: session ? visible(session, actor) : [] };
|
|
82
|
+
}
|
|
83
|
+
function validateMessage(message) {
|
|
84
|
+
if (!message || typeof message !== "object" || Array.isArray(message)) return "Invalid message";
|
|
85
|
+
var text = message.text == null ? "" : message.text;
|
|
86
|
+
if (typeof text !== "string" || text.length > MAX_TEXT) return "Message text is invalid or too long";
|
|
87
|
+
var pastes = message.pastes == null ? [] : message.pastes;
|
|
88
|
+
if (!Array.isArray(pastes) || pastes.length > MAX_PASTES) return "Too many paste attachments";
|
|
89
|
+
for (var i = 0; i < pastes.length; i++) if (typeof pastes[i] !== "string" || pastes[i].length > MAX_PASTE_LENGTH) return "Invalid paste attachment";
|
|
90
|
+
var images = message.images == null ? [] : message.images;
|
|
91
|
+
if (!Array.isArray(images) || images.length > MAX_IMAGES) return "Too many image attachments";
|
|
92
|
+
for (var j = 0; j < images.length; j++) {
|
|
93
|
+
var image = images[j];
|
|
94
|
+
if (!image || typeof image !== "object" || !IMAGE_TYPE_RE.test(image.mediaType || "") || typeof image.data !== "string" || image.data.length > MAX_IMAGE_DATA) return "Invalid image attachment";
|
|
95
|
+
}
|
|
96
|
+
if (!text.trim() && !pastes.length && !images.length) return "Message must contain text or an attachment";
|
|
97
|
+
if (message.clientMessageId != null && (typeof message.clientMessageId !== "string" || !CLIENT_MESSAGE_ID_RE.test(message.clientMessageId))) return "Invalid client message identifier";
|
|
98
|
+
try { if (Buffer.byteLength(JSON.stringify(message), "utf8") > MAX_TOTAL_BYTES) return "Message payload is too large"; }
|
|
99
|
+
catch (e) { return "Invalid message payload"; }
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
function sanitizeImages(images) {
|
|
103
|
+
if (!Array.isArray(images)) return images;
|
|
104
|
+
return images.map(function (image) { return { mediaType: image.mediaType, data: image.data }; });
|
|
105
|
+
}
|
|
106
|
+
function inspect(session, actor, clientMessageId) {
|
|
107
|
+
if (loadError || !clientMessageId) return null;
|
|
108
|
+
for (var i = 0; i < data.items.length; i++) if (data.items[i].clientMessageId === clientMessageId && scoped(data.items[i], session, actor)) return copy(data.items[i]);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
function hasActive(session) {
|
|
112
|
+
if (loadError || !session) return false;
|
|
113
|
+
for (var i = 0; i < data.items.length; i++) if (sameTarget(data.items[i], session) && (data.items[i].state === "pending" || data.items[i].state === "claimed")) return true;
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
function admit(session, message, actor) {
|
|
117
|
+
if (loadError) return result(false, loadError, session, actor);
|
|
118
|
+
if (!session || !canMutate(session, actor, null)) return result(false, "Not authorized", session, actor);
|
|
119
|
+
var validationError = validateMessage(message);
|
|
120
|
+
if (validationError) return result(false, validationError, session, actor);
|
|
121
|
+
var activeCount = 0;
|
|
122
|
+
for (var activeIndex = 0; activeIndex < data.items.length; activeIndex++) {
|
|
123
|
+
if (sameTarget(data.items[activeIndex], session) && (data.items[activeIndex].state === "pending" || data.items[activeIndex].state === "claimed")) activeCount++;
|
|
124
|
+
}
|
|
125
|
+
if (activeCount >= MAX_ACTIVE_ITEMS) return result(false, "Pending message queue is full", session, actor);
|
|
126
|
+
var clientId = message.clientMessageId ? String(message.clientMessageId) : null;
|
|
127
|
+
var duplicate = inspect(session, actor, clientId);
|
|
128
|
+
if (duplicate) return result(duplicate.state !== "cancelled" && duplicate.state !== "failed", duplicate.state === "cancelled" ? "Message was cancelled" : duplicate.state === "failed" ? "Message delivery failed" : null, session, actor);
|
|
129
|
+
var before = copy(data);
|
|
130
|
+
var storedMessage = copy(message);
|
|
131
|
+
if (Object.prototype.hasOwnProperty.call(storedMessage, "images")) storedMessage.images = sanitizeImages(storedMessage.images);
|
|
132
|
+
var item = { id: "pm_" + data.nextId++, sessionKey: key(session), sessionLocalId: session.localId, projectSlug: opts.slug, actorId: actorId(actor), actorName: actor && (actor.displayName || actor.username || actor.name) ? String(actor.displayName || actor.username || actor.name).slice(0, 120) : null, clientMessageId: clientId, message: storedMessage, createdAt: Date.now(), state: "pending" };
|
|
133
|
+
delete item.message._pendingConsumed;
|
|
134
|
+
data.items.push(item);
|
|
135
|
+
data.revision++;
|
|
136
|
+
try { persist(); } catch (e) { restore(before); return result(false, "Queue storage failed", session, actor); }
|
|
137
|
+
broadcast(session, { type: "pending_message_queued", sessionId: session.localId, projectSlug: opts.slug, paused: isPaused(session), item: copy(item), revision: data.revision });
|
|
138
|
+
return result(true, null, session, actor);
|
|
139
|
+
}
|
|
140
|
+
function mutate(session, actor, id, expectedRevision, expectedSessionId, operation, allowedStates) {
|
|
141
|
+
if (loadError) return result(false, loadError, session, actor);
|
|
142
|
+
if (!session || session.localId !== expectedSessionId || !authorize(session, actor)) return result(false, "Session changed", session, actor);
|
|
143
|
+
if (expectedRevision !== data.revision) return result(false, "Stale queue revision", session, actor);
|
|
144
|
+
var index = -1;
|
|
145
|
+
for (var i = 0; i < data.items.length; i++) if (data.items[i].id === id && sameTarget(data.items[i], session)) index = i;
|
|
146
|
+
var mutableStates = allowedStates || ["pending"];
|
|
147
|
+
if (index < 0 || mutableStates.indexOf(data.items[index].state) < 0 || !canMutate(session, actor, data.items[index])) return result(false, "Pending message not found", session, actor);
|
|
148
|
+
var before = copy(data);
|
|
149
|
+
operation(data.items, index);
|
|
150
|
+
data.revision++;
|
|
151
|
+
try { persist(); } catch (e) { restore(before); return result(false, "Queue storage failed", session, actor); }
|
|
152
|
+
if (data.items[index].state === "cancelled" && session._pendingMessageClaimId === data.items[index].id) delete session._pendingMessageClaimId;
|
|
153
|
+
broadcast(session, { type: "pending_message_state", sessionId: session.localId, projectSlug: opts.slug, paused: isPaused(session), revision: data.revision, items: visible(session, actor) });
|
|
154
|
+
return result(true, null, session, actor);
|
|
155
|
+
}
|
|
156
|
+
function edit(session, actor, id, revision, expectedSessionId, message) {
|
|
157
|
+
var current = null;
|
|
158
|
+
for (var i = 0; i < data.items.length; i++) if (data.items[i].id === id && sameTarget(data.items[i], session)) current = data.items[i];
|
|
159
|
+
var text = message && message.text;
|
|
160
|
+
if (typeof text !== "string" || text.length > MAX_TEXT) return result(false, "Edited text is invalid or too long", session, actor);
|
|
161
|
+
var proposed = current ? Object.assign({}, current.message, { text: text }) : null;
|
|
162
|
+
if (message && Object.prototype.hasOwnProperty.call(message, "images")) {
|
|
163
|
+
proposed = Object.assign({}, proposed, { images: message.images });
|
|
164
|
+
}
|
|
165
|
+
if (current) {
|
|
166
|
+
var validationError = validateMessage(proposed);
|
|
167
|
+
if (validationError) return result(false, validationError, session, actor);
|
|
168
|
+
}
|
|
169
|
+
return mutate(session, actor, id, revision, expectedSessionId, function (items, index) {
|
|
170
|
+
var nextMessage = Object.assign({}, items[index].message, { text: text });
|
|
171
|
+
if (message && Object.prototype.hasOwnProperty.call(message, "images")) nextMessage.images = sanitizeImages(message.images);
|
|
172
|
+
items[index].message = nextMessage;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function cancel(session, actor, id, revision, expectedSessionId) {
|
|
176
|
+
return mutate(session, actor, id, revision, expectedSessionId, function (items, index) { items[index].state = "cancelled"; }, ["pending", "claimed"]);
|
|
177
|
+
}
|
|
178
|
+
function reorder(session, actor, ids, revision, expectedSessionId) {
|
|
179
|
+
if (loadError) return result(false, loadError, session, actor);
|
|
180
|
+
var positions = [];
|
|
181
|
+
var pending = [];
|
|
182
|
+
for (var i = 0; i < data.items.length; i++) if (scoped(data.items[i], session, actor) && data.items[i].state === "pending") { positions.push(i); pending.push(data.items[i].id); }
|
|
183
|
+
if (!Array.isArray(ids) || ids.length !== pending.length) return result(false, "Reorder must include every owned pending message", session, actor);
|
|
184
|
+
var seen = {};
|
|
185
|
+
for (var j = 0; j < ids.length; j++) {
|
|
186
|
+
if (seen[ids[j]] || pending.indexOf(ids[j]) < 0) return result(false, "Reorder contains a duplicate, foreign, or nonpending message", session, actor);
|
|
187
|
+
seen[ids[j]] = true;
|
|
188
|
+
}
|
|
189
|
+
if (!ids.length) return result(true, null, session, actor);
|
|
190
|
+
return mutate(session, actor, ids[0], revision, expectedSessionId, function (items) {
|
|
191
|
+
var byId = {};
|
|
192
|
+
for (var k = 0; k < positions.length; k++) byId[items[positions[k]].id] = items[positions[k]];
|
|
193
|
+
for (var n = 0; n < positions.length; n++) items[positions[n]] = byId[ids[n]];
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function pause(session, actor) {
|
|
197
|
+
if (loadError || !session || !authorize(session, actor)) return false;
|
|
198
|
+
if (!hasActive(session)) return true;
|
|
199
|
+
if (isPaused(session)) return true;
|
|
200
|
+
var before = copy(data);
|
|
201
|
+
data.paused[targetKey(session)] = true;
|
|
202
|
+
data.revision++;
|
|
203
|
+
try { persist(); } catch (e) { restore(before); return false; }
|
|
204
|
+
broadcast(session, { type: "pending_message_state", sessionId: session.localId, projectSlug: opts.slug, paused: true, revision: data.revision, items: visible(session, actor) });
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
function resume(session, actor, expectedRevision, expectedSessionId) {
|
|
208
|
+
if (loadError) return result(false, loadError, session, actor);
|
|
209
|
+
if (!session || session.localId !== expectedSessionId || !authorize(session, actor)) return result(false, "Session changed", session, actor);
|
|
210
|
+
if (expectedRevision !== data.revision) return result(false, "Stale queue revision", session, actor);
|
|
211
|
+
if (!isPaused(session)) return result(true, null, session, actor);
|
|
212
|
+
var before = copy(data);
|
|
213
|
+
delete data.paused[targetKey(session)];
|
|
214
|
+
data.revision++;
|
|
215
|
+
try { persist(); } catch (e) { restore(before); return result(false, "Queue storage failed", session, actor); }
|
|
216
|
+
broadcast(session, { type: "pending_message_state", sessionId: session.localId, projectSlug: opts.slug, paused: false, revision: data.revision, items: visible(session, actor) });
|
|
217
|
+
return result(true, null, session, actor);
|
|
218
|
+
}
|
|
219
|
+
function finishClaim(session, id, ok, error) {
|
|
220
|
+
var index = -1;
|
|
221
|
+
for (var i = 0; i < data.items.length; i++) if (data.items[i].id === id && sameTarget(data.items[i], session) && data.items[i].state === "claimed") index = i;
|
|
222
|
+
if (index < 0) return false;
|
|
223
|
+
var before = copy(data);
|
|
224
|
+
data.items[index].state = ok === "release" ? "pending" : ok ? "consumed" : "failed";
|
|
225
|
+
if (ok === "release") delete data.items[index].claimedAt;
|
|
226
|
+
else data.items[index][ok ? "consumedAt" : "failedAt"] = Date.now();
|
|
227
|
+
if (!ok) data.items[index].failure = error || "Pending message dispatch was rejected";
|
|
228
|
+
data.revision++;
|
|
229
|
+
try { persist(); } catch (e) { restore(before); return false; }
|
|
230
|
+
if (session._pendingMessageClaimId === id) delete session._pendingMessageClaimId;
|
|
231
|
+
broadcast(session, { type: ok === "release" ? "pending_message_state" : "pending_message_consumed", sessionId: session.localId, projectSlug: opts.slug, paused: isPaused(session), id: id, state: data.items[index].state, items: ok === "release" ? visible(session, { id: data.items[index].actorId }) : undefined, revision: data.revision });
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
function consumeOne(session, dispatch) {
|
|
235
|
+
if (loadError || !session || session._pendingMessageClaimId || isPaused(session) || session._pendingMessageDrainPaused || session.taskStopRequested || session._queryStarting || session._awaitingTurnResult || session.isProcessing) return null;
|
|
236
|
+
if (session.rateLimitResetsAt && session.rateLimitResetsAt > Date.now()) return null;
|
|
237
|
+
for (var i = 0; i < data.items.length; i++) {
|
|
238
|
+
var item = data.items[i];
|
|
239
|
+
if (!sameTarget(item, session) || item.state !== "pending") continue;
|
|
240
|
+
if (!canMutate(session, { id: item.actorId }, item)) continue;
|
|
241
|
+
var before = copy(data);
|
|
242
|
+
item.state = "claimed";
|
|
243
|
+
item.claimedAt = Date.now();
|
|
244
|
+
data.revision++;
|
|
245
|
+
try { persist(); } catch (e) { restore(before); return null; }
|
|
246
|
+
session._pendingMessageClaimId = item.id;
|
|
247
|
+
broadcast(session, { type: "pending_message_claimed", sessionId: session.localId, projectSlug: opts.slug, paused: isPaused(session), id: item.id, revision: data.revision });
|
|
248
|
+
var completed = false;
|
|
249
|
+
function complete(ok, error) { if (completed) return false; completed = true; return finishClaim(session, item.id, ok, error); }
|
|
250
|
+
try { if (dispatch(copy(item), complete) === false) complete(false, "Pending message dispatch was rejected"); }
|
|
251
|
+
catch (e) { complete(false, e.message || String(e)); }
|
|
252
|
+
return copy(item);
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
function hydrate(session, actor) { return result(!loadError, loadError, session, actor); }
|
|
257
|
+
function isClaimed(session, id) {
|
|
258
|
+
for (var i = 0; i < data.items.length; i++) if (data.items[i].id === id && sameTarget(data.items[i], session) && data.items[i].state === "claimed") return true;
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
return { admit: admit, edit: edit, cancel: cancel, reorder: reorder, pause: pause, resume: resume, consumeOne: consumeOne, hydrate: hydrate, inspect: inspect, hasActive: hasActive, isClaimed: isClaimed, isPaused: isPaused, list: visible, getRevision: function () { return data.revision; }, getLoadError: function () { return loadError; }, filePath: filePath };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = { createPendingMessageQueue: createPendingMessageQueue };
|
|
@@ -266,12 +266,10 @@ function attachScheduledTasks(ctx) {
|
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
async function engineState(ws, msg, session) {
|
|
269
|
-
var userId = ws && ws._clayUser ? ws._clayUser.id : "default";
|
|
270
|
-
var preference = ctx.usersModule && ctx.usersModule.getScheduledTaskInterviewEngine ? ctx.usersModule.getScheduledTaskInterviewEngine(userId) : {};
|
|
271
269
|
var installed = (sm.installedVendors || []).filter(function (vendor) { return sessionToolTransport.capability(vendor).kind !== "none"; });
|
|
272
270
|
var runtimeInstalled = sm.installedVendors || [];
|
|
273
271
|
var requestedVendor = clean(msg && msg.vendor, 40);
|
|
274
|
-
var targetVendor = runtimeInstalled.indexOf(requestedVendor) !== -1 ? requestedVendor :
|
|
272
|
+
var targetVendor = runtimeInstalled.indexOf(requestedVendor) !== -1 ? requestedVendor : installed[0] || runtimeInstalled[0] || "";
|
|
275
273
|
var catalogError = "";
|
|
276
274
|
if (targetVendor && ctx.getEngineCatalog) {
|
|
277
275
|
try {
|
|
@@ -285,7 +283,7 @@ function attachScheduledTasks(ctx) {
|
|
|
285
283
|
models[runtimeInstalled[i]] = (sm.modelsByVendor && sm.modelsByVendor[runtimeInstalled[i]] || []).slice();
|
|
286
284
|
ready[runtimeInstalled[i]] = models[runtimeInstalled[i]].length > 0;
|
|
287
285
|
}
|
|
288
|
-
ctx.sendTo(ws, { type: "scheduled_task_interview_engine_state", requestId: msg.requestId || null, projectSlug: ctx.projectSlug || null, sessionId: session.localId, installedVendors: installed, runtimeInstalledVendors: runtimeInstalled.slice(), modelsByVendor: models, catalogReadyByVendor: ready, catalogVendor: targetVendor, error: catalogError
|
|
286
|
+
ctx.sendTo(ws, { type: "scheduled_task_interview_engine_state", requestId: msg.requestId || null, projectSlug: ctx.projectSlug || null, sessionId: session.localId, installedVendors: installed, runtimeInstalledVendors: runtimeInstalled.slice(), modelsByVendor: models, catalogReadyByVendor: ready, catalogVendor: targetVendor, error: catalogError });
|
|
289
287
|
}
|
|
290
288
|
|
|
291
289
|
function createTaskFiles(id, task) {
|
package/lib/project-sessions.js
CHANGED
|
@@ -108,8 +108,11 @@ function attachSessions(ctx) {
|
|
|
108
108
|
var getLinuxUserForSession = ctx.getLinuxUserForSession;
|
|
109
109
|
var ensureProjectAccessForSession = ctx.ensureProjectAccessForSession;
|
|
110
110
|
var getOsUserInfoForWs = ctx.getOsUserInfoForWs;
|
|
111
|
-
function stopSession(session) {
|
|
112
|
-
if (!session
|
|
111
|
+
function stopSession(session, actor) {
|
|
112
|
+
if (!session) return false;
|
|
113
|
+
session._pendingMessageDrainPaused = true;
|
|
114
|
+
if (typeof ctx.onPendingQueuePause === "function") ctx.onPendingQueuePause(session, actor || null);
|
|
115
|
+
if (!session.isProcessing && !session._queryStarting) return true;
|
|
113
116
|
if (typeof ctx.onHumanPairStop === "function") ctx.onHumanPairStop(session);
|
|
114
117
|
session.taskStopRequested = true;
|
|
115
118
|
if (session.abortController) session.abortController.abort();
|
|
@@ -506,6 +509,64 @@ function attachSessions(ctx) {
|
|
|
506
509
|
}
|
|
507
510
|
}
|
|
508
511
|
|
|
512
|
+
function scheduledRequestError(ws, msg, text) {
|
|
513
|
+
if (!msg.requestId) return;
|
|
514
|
+
sendTo(ws, { type: "new_session_result", requestId: String(msg.requestId).substring(0, 200), ok: false, error: text });
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function scheduledSourceIsCurrent(ws, msg, sourceSession) {
|
|
518
|
+
if (!sourceSession || getSessionForWs(ws) !== sourceSession) return false;
|
|
519
|
+
if (typeof msg.sourceSessionId !== "number" || msg.sourceSessionId !== sourceSession.localId) return false;
|
|
520
|
+
if (msg.projectSlug !== slug || msg.mode !== "gui" || msg.forceNew !== true) return false;
|
|
521
|
+
return !!ctx.canCreateScheduledTaskInterview && ctx.canCreateScheduledTaskInterview(ws, sourceSession) === true;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function prepareScheduledInterview(ws, msg) {
|
|
525
|
+
var sourceSession = getSessionForWs(ws);
|
|
526
|
+
var actorId = usersModule.isMultiUser() ? (ws._clayUser && ws._clayUser.id) : "default";
|
|
527
|
+
if (!actorId || !scheduledSourceIsCurrent(ws, msg, sourceSession)) throw new Error("Scheduled Tasks is unavailable in the current project session.");
|
|
528
|
+
if (typeof ctx.resolveDefaultAi !== "function") throw new Error("Default AI is unavailable for assisted scheduling.");
|
|
529
|
+
var resolved = await ctx.resolveDefaultAi(ws);
|
|
530
|
+
if (usersModule.isMultiUser()) {
|
|
531
|
+
var liveId = ws._clayUser && ws._clayUser.id;
|
|
532
|
+
if (liveId !== actorId || !usersModule.findUserById(actorId)) throw new Error("Your account is no longer available.");
|
|
533
|
+
}
|
|
534
|
+
if (!scheduledSourceIsCurrent(ws, msg, sourceSession)) throw new Error("Scheduled Tasks is no longer available in the current project session.");
|
|
535
|
+
if (!resolved || !resolved.ready) throw new Error(resolved && resolved.error || "Default AI is unavailable for assisted scheduling.");
|
|
536
|
+
var installed = sm.installedVendors || [];
|
|
537
|
+
var vendorInfo = yoke.getVendorInfo(resolved.vendor);
|
|
538
|
+
if (installed.indexOf(resolved.vendor) === -1 || !vendorInfo || yoke.sessionTools.capability(resolved.vendor).kind === "none") {
|
|
539
|
+
throw new Error("The Default AI provider cannot run the Scheduled Tasks interview tools in this project.");
|
|
540
|
+
}
|
|
541
|
+
var models = resolved.catalog && resolved.catalog.models || [];
|
|
542
|
+
if (!catalogHasModel(models, resolved.model)) throw new Error("The Default AI model is no longer available.");
|
|
543
|
+
if (!catalogSupportsEffort(models, resolved.model, resolved.effort || "", vendorInfo.effortLevels)) throw new Error("The Default AI effort is unavailable for this model.");
|
|
544
|
+
return { sourceSession: sourceSession, actorId: actorId, runtime: resolved };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function createScheduledInterview(ws, msg, prepared) {
|
|
548
|
+
if (!scheduledSourceIsCurrent(ws, msg, prepared.sourceSession)) throw new Error("Scheduled Tasks is no longer available in the current project session.");
|
|
549
|
+
if (usersModule.isMultiUser()) {
|
|
550
|
+
var liveId = ws._clayUser && ws._clayUser.id;
|
|
551
|
+
if (liveId !== prepared.actorId || !usersModule.findUserById(prepared.actorId)) throw new Error("Your account is no longer available.");
|
|
552
|
+
}
|
|
553
|
+
sm.sweepBlankSessions();
|
|
554
|
+
var sessionOpts = {
|
|
555
|
+
sessionVisibility: sessionVisibility.normalize(msg.sessionVisibility),
|
|
556
|
+
vendor: prepared.runtime.vendor,
|
|
557
|
+
model: prepared.runtime.model,
|
|
558
|
+
effort: prepared.runtime.effort || null,
|
|
559
|
+
mode: "gui",
|
|
560
|
+
};
|
|
561
|
+
if (usersModule.isMultiUser()) sessionOpts.ownerId = prepared.actorId;
|
|
562
|
+
var newSession = sm.createSession(sessionOpts, ws);
|
|
563
|
+
ws._clayActiveSession = newSession.localId;
|
|
564
|
+
if (msg.requestId) sendTo(ws, { type: "new_session_result", requestId: String(msg.requestId).substring(0, 200), ok: true, sessionId: newSession.localId });
|
|
565
|
+
var presenceKey = ws._clayUser ? ws._clayUser.id : "_default";
|
|
566
|
+
if (!ws._clayPane) userPresence.setPresence(slug, presenceKey, userPresence.sessionIdForPersistence(newSession), null);
|
|
567
|
+
if (usersModule.isMultiUser() && !ws._clayPane) broadcastPresence();
|
|
568
|
+
}
|
|
569
|
+
|
|
509
570
|
function handleSessionsMessage(ws, msg) {
|
|
510
571
|
|
|
511
572
|
if (msg.type === "push_subscribe") {
|
|
@@ -532,32 +593,22 @@ function attachSessions(ctx) {
|
|
|
532
593
|
|
|
533
594
|
if (msg.type === "new_session") {
|
|
534
595
|
if (msg.scheduleInterview === true) {
|
|
535
|
-
var
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
var knownModels = sm.modelsByVendor && sm.modelsByVendor[interviewVendor] || [];
|
|
539
|
-
var requestedModel = typeof msg.model === "string" ? msg.model : "";
|
|
540
|
-
var requestedEffort = typeof msg.effort === "string" ? msg.effort : "";
|
|
541
|
-
var vendorInfo = yoke.getVendorInfo(interviewVendor);
|
|
542
|
-
var engineError = "";
|
|
543
|
-
var sourceMatches = !!sourceSession && Number(msg.sourceSessionId) === Number(sourceSession.localId);
|
|
544
|
-
if (!sourceMatches || msg.projectSlug !== slug || !ctx.canCreateScheduledTaskInterview || !ctx.canCreateScheduledTaskInterview(ws, sourceSession)) engineError = "Scheduled Tasks is unavailable in the current project session.";
|
|
545
|
-
else if (!interviewVendor || installed.indexOf(interviewVendor) === -1 || !vendorInfo || yoke.sessionTools.capability(interviewVendor).kind === "none") engineError = "The selected interview engine is no longer available for assisted scheduling.";
|
|
546
|
-
else if (!knownModels.length) engineError = "Interview models are still loading or unavailable for this engine.";
|
|
547
|
-
else if (requestedModel && !catalogHasModel(knownModels, requestedModel)) engineError = "The selected interview model is no longer available.";
|
|
548
|
-
else if (!catalogSupportsEffort(knownModels, requestedModel, requestedEffort, vendorInfo.effortLevels)) engineError = "The selected interview effort is unavailable for this engine.";
|
|
549
|
-
if (engineError) {
|
|
550
|
-
if (msg.requestId) sendTo(ws, { type: "new_session_result", requestId: String(msg.requestId).substring(0, 200), ok: false, error: engineError });
|
|
551
|
-
return true;
|
|
552
|
-
}
|
|
553
|
-
var preferenceUserId = ws._clayUser ? ws._clayUser.id : "default";
|
|
554
|
-
var savedPreference;
|
|
555
|
-
try { savedPreference = usersModule.setScheduledTaskInterviewEngine(preferenceUserId, { vendor: interviewVendor, model: requestedModel, effort: requestedEffort }); }
|
|
556
|
-
catch (preferenceError) { savedPreference = { error: preferenceError.message || String(preferenceError) }; }
|
|
557
|
-
if (!savedPreference || !savedPreference.ok) {
|
|
558
|
-
if (msg.requestId) sendTo(ws, { type: "new_session_result", requestId: String(msg.requestId).substring(0, 200), ok: false, error: savedPreference && savedPreference.error || "The interview engine preference could not be saved." });
|
|
596
|
+
var scheduledRequestId = typeof msg.requestId === "string" ? msg.requestId.substring(0, 200) : "";
|
|
597
|
+
if (!scheduledRequestId) {
|
|
598
|
+
scheduledRequestError(ws, msg, "A request identity is required for a Scheduled Tasks interview.");
|
|
559
599
|
return true;
|
|
560
600
|
}
|
|
601
|
+
if (!ws._scheduledInterviewSessionRequests) ws._scheduledInterviewSessionRequests = {};
|
|
602
|
+
if (ws._scheduledInterviewSessionRequests[scheduledRequestId]) return true;
|
|
603
|
+
var pendingScheduled = prepareScheduledInterview(ws, msg).then(function (prepared) {
|
|
604
|
+
createScheduledInterview(ws, msg, prepared);
|
|
605
|
+
}).catch(function (error) {
|
|
606
|
+
scheduledRequestError(ws, msg, error.message || String(error));
|
|
607
|
+
}).finally(function () {
|
|
608
|
+
if (ws._scheduledInterviewSessionRequests[scheduledRequestId] === pendingScheduled) delete ws._scheduledInterviewSessionRequests[scheduledRequestId];
|
|
609
|
+
});
|
|
610
|
+
ws._scheduledInterviewSessionRequests[scheduledRequestId] = pendingScheduled;
|
|
611
|
+
return true;
|
|
561
612
|
}
|
|
562
613
|
// Opportunistic hygiene: drop blanks that sat untouched past the grace
|
|
563
614
|
// period before deciding whether to reuse or create.
|
|
@@ -566,7 +617,6 @@ function attachSessions(ctx) {
|
|
|
566
617
|
if (ws._clayUser && usersModule.isMultiUser()) sessionOpts.ownerId = ws._clayUser.id;
|
|
567
618
|
sessionOpts.sessionVisibility = sessionVisibility.normalize(msg.sessionVisibility);
|
|
568
619
|
if (msg.vendor) sessionOpts.vendor = msg.vendor;
|
|
569
|
-
if (msg.scheduleInterview === true && msg.model) sessionOpts.model = msg.model;
|
|
570
620
|
// Mode resolution: vendors without a TUI session mode are always GUI.
|
|
571
621
|
// TUI-capable sessions honor the explicit msg.mode if provided, otherwise
|
|
572
622
|
// fall back to the user's claudeOpenMode preference. This is what
|
|
@@ -636,9 +686,7 @@ function attachSessions(ctx) {
|
|
|
636
686
|
sm.switchSession(newSess.localId, ws);
|
|
637
687
|
} else {
|
|
638
688
|
var newSessionVendor = sessionOpts.vendor || sm.defaultVendor || "claude";
|
|
639
|
-
sessionOpts.effort =
|
|
640
|
-
? msg.effort
|
|
641
|
-
: yoke.clampEffort(newSessionVendor, (sm.currentEffortByVendor && sm.currentEffortByVendor[newSessionVendor]) || sm.currentEffort || "medium") || null;
|
|
689
|
+
sessionOpts.effort = yoke.clampEffort(newSessionVendor, (sm.currentEffortByVendor && sm.currentEffortByVendor[newSessionVendor]) || sm.currentEffort || "medium") || null;
|
|
642
690
|
// Reuse an existing blank GUI session instead of stacking another
|
|
643
691
|
// one. A vendor-less blank is acceptable: nothing has happened in it,
|
|
644
692
|
// so stamping the requested vendor is equivalent to a fresh create.
|
|
@@ -1073,7 +1121,7 @@ function attachSessions(ctx) {
|
|
|
1073
1121
|
if (msg.type === "stop") {
|
|
1074
1122
|
var session = getSessionForWs(ws);
|
|
1075
1123
|
if (autonomousRun && session && session.autonomousRun) autonomousRun.finish(session, "stopped", "Stopped by the user.");
|
|
1076
|
-
stopSession(session);
|
|
1124
|
+
stopSession(session, ws && ws._clayUser);
|
|
1077
1125
|
return true;
|
|
1078
1126
|
}
|
|
1079
1127
|
|
|
@@ -1484,6 +1532,9 @@ function attachSessions(ctx) {
|
|
|
1484
1532
|
if (msg.type === "cursor_move" || msg.type === "cursor_leave" || msg.type === "text_select") {
|
|
1485
1533
|
if (!usersModule.isMultiUser() || !ws._clayUser) return true;
|
|
1486
1534
|
var u = ws._clayUser;
|
|
1535
|
+
if (typeof usersModule.getCursorSharing === "function" && !usersModule.getCursorSharing(u.id)) {
|
|
1536
|
+
if (msg.type !== "cursor_leave" && !(msg.type === "text_select" && Array.isArray(msg.ranges) && msg.ranges.length === 0)) return true;
|
|
1537
|
+
}
|
|
1487
1538
|
var p = u.profile || {};
|
|
1488
1539
|
var cursorMsg = {
|
|
1489
1540
|
type: msg.type,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Keeps Capsule LLM selection and execution bound to one live project actor.
|
|
2
|
+
|
|
3
|
+
function attachProjectToolLlmRuntime(ctx) {
|
|
4
|
+
function currentPrincipal(ws) {
|
|
5
|
+
if (!ctx.users.isMultiUser() && !ctx.osUsers) return { userId: "default", linuxUser: null };
|
|
6
|
+
var claimedId = ws && ws._clayUser && ws._clayUser.id;
|
|
7
|
+
var user = claimedId ? ctx.users.findUserById(claimedId) : null;
|
|
8
|
+
if (!user) throw new Error("Authenticated user identity is required for this AI request.");
|
|
9
|
+
var access = ctx.getProjectAccess();
|
|
10
|
+
if (!ctx.users.canAccessProject(user.id, access)) throw new Error("Project access is no longer available for this AI request.");
|
|
11
|
+
var linuxUser = ctx.osUsers ? user.linuxUser || null : null;
|
|
12
|
+
if (ctx.osUsers && !linuxUser) throw new Error("An OS user identity is required for this AI request.");
|
|
13
|
+
return { userId: user.id, linuxUser: linuxUser };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function revalidatePrincipal(ws, expected) {
|
|
17
|
+
var current = currentPrincipal(ws);
|
|
18
|
+
if (current.userId !== expected.userId || current.linuxUser !== expected.linuxUser) {
|
|
19
|
+
throw new Error("The authenticated runtime identity changed while the AI request was loading.");
|
|
20
|
+
}
|
|
21
|
+
return current;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function getConfiguration(ws, alias) {
|
|
25
|
+
var principal = currentPrincipal(ws);
|
|
26
|
+
var selection = await ctx.resolveModel(ws, alias);
|
|
27
|
+
revalidatePrincipal(ws, principal);
|
|
28
|
+
return selection;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function complete(ws, args) {
|
|
32
|
+
var principal = currentPrincipal(ws);
|
|
33
|
+
var selection = await ctx.resolveModel(ws, args.model);
|
|
34
|
+
var live = revalidatePrincipal(ws, principal);
|
|
35
|
+
return ctx.complete({
|
|
36
|
+
adapters: ctx.adapters,
|
|
37
|
+
cwd: ctx.cwd,
|
|
38
|
+
linuxUser: live.linuxUser,
|
|
39
|
+
args: args,
|
|
40
|
+
selection: selection,
|
|
41
|
+
timeoutMs: 60000,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { getConfiguration: getConfiguration, complete: complete };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { attachProjectToolLlmRuntime: attachProjectToolLlmRuntime };
|