clay-server 4.1.0-beta.7 → 4.1.0-beta.9
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/bin/cli.js +1 -1
- package/lib/project-pending-message-queue.js +71 -7
- package/lib/project-scheduled-messages.js +235 -52
- package/lib/project-user-message.js +25 -5
- package/lib/project.js +7 -2
- package/lib/public/app.js +1 -3
- package/lib/public/css/input.css +0 -109
- package/lib/public/css/pending-message-queue.css +3 -0
- package/lib/public/modules/app-messages.js +6 -25
- package/lib/public/modules/app-projects.js +1 -0
- package/lib/public/modules/app-rate-limit.js +0 -182
- package/lib/public/modules/chat-render-runtime.js +0 -2
- package/lib/public/modules/input.js +81 -20
- package/lib/public/modules/pending-message-queue.js +43 -1
- package/lib/public/modules/scheduled-message-state.js +12 -36
- package/lib/session-pair-mcp-server.js +5 -4
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -1268,7 +1268,7 @@ function setup(callback) {
|
|
|
1268
1268
|
console.clear();
|
|
1269
1269
|
printLogo();
|
|
1270
1270
|
log("");
|
|
1271
|
-
log(sym.pointer + " " + a.bold + "Clay" + a.reset + a.dim + " ·
|
|
1271
|
+
log(sym.pointer + " " + a.bold + "Clay Studio" + a.reset + a.dim + " · Official open-source distribution" + a.reset);
|
|
1272
1272
|
log(sym.bar);
|
|
1273
1273
|
log(sym.bar + " " + a.yellow + sym.warn + " Disclaimer" + a.reset);
|
|
1274
1274
|
log(sym.bar);
|
|
@@ -37,6 +37,7 @@ function createPendingMessageQueue(opts) {
|
|
|
37
37
|
for (var i = 0; i < parsed.items.length; i++) {
|
|
38
38
|
var item = parsed.items[i];
|
|
39
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
|
+
if (item.schedule !== undefined && (!item.schedule || typeof item.schedule !== "object" || typeof item.schedule.jobId !== "string" || typeof item.schedule.revision !== "string" || typeof item.schedule.ready !== "boolean" || !Number.isFinite(Number(item.schedule.notBefore)))) return false;
|
|
40
41
|
}
|
|
41
42
|
return true;
|
|
42
43
|
}
|
|
@@ -113,30 +114,42 @@ function createPendingMessageQueue(opts) {
|
|
|
113
114
|
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
115
|
return false;
|
|
115
116
|
}
|
|
116
|
-
function
|
|
117
|
+
function admitInternal(session, message, actor, trustedSchedule) {
|
|
117
118
|
if (loadError) return result(false, loadError, session, actor);
|
|
118
119
|
if (!session || !canMutate(session, actor, null)) return result(false, "Not authorized", session, actor);
|
|
119
120
|
var validationError = validateMessage(message);
|
|
120
121
|
if (validationError) return result(false, validationError, session, actor);
|
|
122
|
+
var clientId = message.clientMessageId ? String(message.clientMessageId) : null;
|
|
123
|
+
var duplicate = inspect(session, actor, clientId);
|
|
124
|
+
if (duplicate) {
|
|
125
|
+
var duplicateResult = result(duplicate.state !== "cancelled" && duplicate.state !== "failed", duplicate.state === "cancelled" ? "Message was cancelled" : duplicate.state === "failed" ? "Message delivery failed" : null, session, actor);
|
|
126
|
+
duplicateResult.item = duplicate;
|
|
127
|
+
return duplicateResult;
|
|
128
|
+
}
|
|
121
129
|
var activeCount = 0;
|
|
122
130
|
for (var activeIndex = 0; activeIndex < data.items.length; activeIndex++) {
|
|
123
131
|
if (sameTarget(data.items[activeIndex], session) && (data.items[activeIndex].state === "pending" || data.items[activeIndex].state === "claimed")) activeCount++;
|
|
124
132
|
}
|
|
125
133
|
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
134
|
var before = copy(data);
|
|
130
135
|
var storedMessage = copy(message);
|
|
136
|
+
delete storedMessage.schedule;
|
|
131
137
|
if (Object.prototype.hasOwnProperty.call(storedMessage, "images")) storedMessage.images = sanitizeImages(storedMessage.images);
|
|
132
138
|
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" };
|
|
139
|
+
if (trustedSchedule) item.schedule = copy(trustedSchedule);
|
|
133
140
|
delete item.message._pendingConsumed;
|
|
141
|
+
delete item.message._scheduledJobId;
|
|
142
|
+
delete item.message._scheduledRevision;
|
|
143
|
+
delete item.message._scheduledNotBefore;
|
|
134
144
|
data.items.push(item);
|
|
135
145
|
data.revision++;
|
|
136
146
|
try { persist(); } catch (e) { restore(before); return result(false, "Queue storage failed", session, actor); }
|
|
137
147
|
broadcast(session, { type: "pending_message_queued", sessionId: session.localId, projectSlug: opts.slug, paused: isPaused(session), item: copy(item), revision: data.revision });
|
|
138
|
-
|
|
148
|
+
var admitted = result(true, null, session, actor);
|
|
149
|
+
admitted.item = copy(item);
|
|
150
|
+
return admitted;
|
|
139
151
|
}
|
|
152
|
+
function admit(session, message, actor) { return admitInternal(session, message, actor, null); }
|
|
140
153
|
function mutate(session, actor, id, expectedRevision, expectedSessionId, operation, allowedStates) {
|
|
141
154
|
if (loadError) return result(false, loadError, session, actor);
|
|
142
155
|
if (!session || session.localId !== expectedSessionId || !authorize(session, actor)) return result(false, "Session changed", session, actor);
|
|
@@ -151,6 +164,7 @@ function createPendingMessageQueue(opts) {
|
|
|
151
164
|
try { persist(); } catch (e) { restore(before); return result(false, "Queue storage failed", session, actor); }
|
|
152
165
|
if (data.items[index].state === "cancelled" && session._pendingMessageClaimId === data.items[index].id) delete session._pendingMessageClaimId;
|
|
153
166
|
broadcast(session, { type: "pending_message_state", sessionId: session.localId, projectSlug: opts.slug, paused: isPaused(session), revision: data.revision, items: visible(session, actor) });
|
|
167
|
+
if (data.items[index].state === "cancelled" && typeof opts.onItemCancelled === "function") opts.onItemCancelled(copy(data.items[index]));
|
|
154
168
|
return result(true, null, session, actor);
|
|
155
169
|
}
|
|
156
170
|
function edit(session, actor, id, revision, expectedSessionId, message) {
|
|
@@ -166,8 +180,9 @@ function createPendingMessageQueue(opts) {
|
|
|
166
180
|
var validationError = validateMessage(proposed);
|
|
167
181
|
if (validationError) return result(false, validationError, session, actor);
|
|
168
182
|
}
|
|
183
|
+
if (current && (!session || session.localId !== expectedSessionId || !authorize(session, actor) || revision !== data.revision || current.state !== "pending" || !canMutate(session, actor, current))) return result(false, "Pending message not found", session, actor);
|
|
169
184
|
return mutate(session, actor, id, revision, expectedSessionId, function (items, index) {
|
|
170
|
-
var nextMessage = Object.assign({}, items[index].message, { text: text });
|
|
185
|
+
var nextMessage = Object.assign({}, items[index].message, proposed || { text: text });
|
|
171
186
|
if (message && Object.prototype.hasOwnProperty.call(message, "images")) nextMessage.images = sanitizeImages(message.images);
|
|
172
187
|
items[index].message = nextMessage;
|
|
173
188
|
});
|
|
@@ -229,6 +244,7 @@ function createPendingMessageQueue(opts) {
|
|
|
229
244
|
try { persist(); } catch (e) { restore(before); return false; }
|
|
230
245
|
if (session._pendingMessageClaimId === id) delete session._pendingMessageClaimId;
|
|
231
246
|
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 });
|
|
247
|
+
if (ok === true && typeof opts.onItemConsumed === "function") opts.onItemConsumed(copy(data.items[index]));
|
|
232
248
|
return true;
|
|
233
249
|
}
|
|
234
250
|
function consumeOne(session, dispatch) {
|
|
@@ -237,6 +253,7 @@ function createPendingMessageQueue(opts) {
|
|
|
237
253
|
for (var i = 0; i < data.items.length; i++) {
|
|
238
254
|
var item = data.items[i];
|
|
239
255
|
if (!sameTarget(item, session) || item.state !== "pending") continue;
|
|
256
|
+
if (item.schedule && !item.schedule.ready) continue;
|
|
240
257
|
if (!canMutate(session, { id: item.actorId }, item)) continue;
|
|
241
258
|
var before = copy(data);
|
|
242
259
|
item.state = "claimed";
|
|
@@ -258,7 +275,54 @@ function createPendingMessageQueue(opts) {
|
|
|
258
275
|
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
276
|
return false;
|
|
260
277
|
}
|
|
261
|
-
|
|
278
|
+
function upsertScheduled(session, message, actor, trustedSchedule) {
|
|
279
|
+
message = copy(message);
|
|
280
|
+
var schedule = trustedSchedule || message.schedule;
|
|
281
|
+
delete message.schedule;
|
|
282
|
+
if (!schedule || typeof schedule.jobId !== "string" || typeof schedule.revision !== "string" || typeof schedule.ready !== "boolean" || !Number.isFinite(Number(schedule.notBefore))) return result(false, "Invalid scheduled queue metadata", session, actor);
|
|
283
|
+
var validationError = validateMessage(message);
|
|
284
|
+
if (validationError) return result(false, validationError, session, actor);
|
|
285
|
+
if (Object.prototype.hasOwnProperty.call(message, "images")) message.images = sanitizeImages(message.images);
|
|
286
|
+
delete message._pendingConsumed;
|
|
287
|
+
delete message._scheduledJobId;
|
|
288
|
+
delete message._scheduledRevision;
|
|
289
|
+
delete message._scheduledNotBefore;
|
|
290
|
+
for (var i = 0; i < data.items.length; i++) {
|
|
291
|
+
if (sameTarget(data.items[i], session) && data.items[i].schedule && data.items[i].schedule.jobId === schedule.jobId && data.items[i].state !== "pending") return result(false, "Scheduled queue item is terminal", session, actor);
|
|
292
|
+
if (sameTarget(data.items[i], session) && data.items[i].schedule && data.items[i].schedule.jobId === schedule.jobId && data.items[i].state === "pending") {
|
|
293
|
+
if (!authorize(session, actor) || !canMutate(session, actor, data.items[i])) return result(false, "Not authorized", session, actor);
|
|
294
|
+
var before = copy(data);
|
|
295
|
+
data.items[i].message = copy(message);
|
|
296
|
+
data.items[i].schedule = copy(schedule);
|
|
297
|
+
data.revision++;
|
|
298
|
+
try { persist(); } catch (error) { restore(before); return result(false, "Queue storage failed", session, actor); }
|
|
299
|
+
broadcast(session, { type: "pending_message_state", sessionId: session.localId, projectSlug: opts.slug, paused: isPaused(session), revision: data.revision, items: visible(session, actor) });
|
|
300
|
+
return { ok: true, item: copy(data.items[i]), revision: data.revision };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return admitInternal(session, message, actor, schedule);
|
|
304
|
+
}
|
|
305
|
+
function cancelScheduled(session, jobId, actor) {
|
|
306
|
+
for (var i = 0; i < data.items.length; i++) {
|
|
307
|
+
if (sameTarget(data.items[i], session) && data.items[i].schedule && data.items[i].schedule.jobId === jobId && (data.items[i].state === "pending" || data.items[i].state === "claimed")) return cancel(session, actor, data.items[i].id, data.revision, session.localId);
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
function releaseScheduled(session, jobId, revision) {
|
|
312
|
+
for (var i = 0; i < data.items.length; i++) {
|
|
313
|
+
var item = data.items[i];
|
|
314
|
+
if (!sameTarget(item, session) || !item.schedule || item.schedule.jobId !== jobId || item.schedule.revision !== revision) continue;
|
|
315
|
+
if (item.state !== "pending" || item.schedule.ready) return item.state === "pending" ? copy(item) : null;
|
|
316
|
+
var before = copy(data);
|
|
317
|
+
item.schedule.ready = true;
|
|
318
|
+
data.revision++;
|
|
319
|
+
try { persist(); } catch (error) { restore(before); return null; }
|
|
320
|
+
broadcast(session, { type: "pending_message_state", sessionId: session.localId, projectSlug: opts.slug, paused: isPaused(session), revision: data.revision, items: visible(session, { id: item.actorId }) });
|
|
321
|
+
return copy(item);
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
return { admit: admit, upsertScheduled: upsertScheduled, releaseScheduled: releaseScheduled, cancelScheduled: cancelScheduled, 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
326
|
}
|
|
263
327
|
|
|
264
328
|
module.exports = { createPendingMessageQueue: createPendingMessageQueue };
|
|
@@ -2,19 +2,19 @@ var crypto = require("crypto");
|
|
|
2
2
|
var sessionProvenance = require("./session-provenance");
|
|
3
3
|
|
|
4
4
|
var LOCAL_OWNER = "local-user";
|
|
5
|
-
|
|
5
|
+
var REQUEST_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
6
6
|
function attachProjectScheduledMessages(ctx) {
|
|
7
7
|
var scheduler = ctx.scheduler;
|
|
8
8
|
var slug = ctx.slug;
|
|
9
9
|
var sm = ctx.sm;
|
|
10
|
-
var sdk = ctx.sdk;
|
|
11
10
|
var users = ctx.usersModule;
|
|
11
|
+
var pendingMessageQueue = ctx.pendingMessageQueue;
|
|
12
|
+
var consumePendingMessage = ctx.consumePendingMessage;
|
|
12
13
|
var type = "project-chat-message:" + slug;
|
|
13
14
|
var makeRevision = ctx.makeRevision || function () { return crypto.randomUUID(); };
|
|
14
15
|
var now = ctx.now || Date.now;
|
|
15
16
|
var unregister = null;
|
|
16
17
|
var stopped = false;
|
|
17
|
-
|
|
18
18
|
function findSession(cliSessionId) {
|
|
19
19
|
var found = null;
|
|
20
20
|
sm.sessions.forEach(function (session) {
|
|
@@ -22,7 +22,6 @@ function attachProjectScheduledMessages(ctx) {
|
|
|
22
22
|
});
|
|
23
23
|
return found;
|
|
24
24
|
}
|
|
25
|
-
|
|
26
25
|
function latestJob(session) {
|
|
27
26
|
if (!scheduler || !session || !session.cliSessionId) return null;
|
|
28
27
|
var jobs = scheduler.listJobs();
|
|
@@ -31,7 +30,6 @@ function attachProjectScheduledMessages(ctx) {
|
|
|
31
30
|
}
|
|
32
31
|
return null;
|
|
33
32
|
}
|
|
34
|
-
|
|
35
33
|
function pendingJob(session) {
|
|
36
34
|
if (!scheduler || !session || !session.cliSessionId) return null;
|
|
37
35
|
if (session.scheduledMessage && session.scheduledMessage.jobId) {
|
|
@@ -45,21 +43,43 @@ function attachProjectScheduledMessages(ctx) {
|
|
|
45
43
|
}
|
|
46
44
|
return null;
|
|
47
45
|
}
|
|
48
|
-
|
|
49
|
-
function projection(job) {
|
|
46
|
+
function projection(job, queueItem) {
|
|
50
47
|
if (!job || job.state !== "queued") return null;
|
|
51
48
|
return {
|
|
52
49
|
jobId: job.id,
|
|
53
50
|
revision: job.payload.revision,
|
|
54
|
-
text:
|
|
51
|
+
text: queueItem && queueItem.message ? queueItem.message.text : "",
|
|
55
52
|
resetsAt: job.runAt,
|
|
56
53
|
scheduledAt: job.payload.scheduledAt,
|
|
57
54
|
};
|
|
58
55
|
}
|
|
59
|
-
|
|
56
|
+
function queueActor(actorId) {
|
|
57
|
+
return users.isMultiUser() ? users.findUserById(actorId) : { id: "default" };
|
|
58
|
+
}
|
|
59
|
+
function queueItemForJob(session, job, actorId) {
|
|
60
|
+
if (!pendingMessageQueue || !session || !job) return null;
|
|
61
|
+
var actor = queueActor(actorId || (job.payload && job.payload.actorId));
|
|
62
|
+
var items = pendingMessageQueue.list(session, actor);
|
|
63
|
+
for (var i = 0; i < items.length; i++) {
|
|
64
|
+
if (items[i].schedule && items[i].schedule.jobId === job.id && items[i].schedule.revision === job.payload.revision) return items[i];
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
function validateQueueControl(session, job, actorId, correlation, requireQueueRevision) {
|
|
69
|
+
if (!pendingMessageQueue) return null;
|
|
70
|
+
if (requireQueueRevision && (!correlation || correlation.queueRevision == null || Number(correlation.queueRevision) !== pendingMessageQueue.getRevision())) {
|
|
71
|
+
throw new Error("The scheduled-message queue is stale.");
|
|
72
|
+
}
|
|
73
|
+
var actor = queueActor(actorId || (job.payload && job.payload.actorId));
|
|
74
|
+
var item = queueItemForJob(session, job, actorId);
|
|
75
|
+
if (!item || item.actorId !== actor.id || ["cancelled", "consumed", "failed"].indexOf(item.state) >= 0) {
|
|
76
|
+
throw new Error("The scheduled queue item is no longer available.");
|
|
77
|
+
}
|
|
78
|
+
return item;
|
|
79
|
+
}
|
|
60
80
|
function sendState(session, targetWs, sendFn) {
|
|
61
81
|
var job = pendingJob(session);
|
|
62
|
-
session.scheduledMessage = projection(job);
|
|
82
|
+
session.scheduledMessage = projection(job, queueItemForJob(session, job, job && job.payload && job.payload.actorId));
|
|
63
83
|
var latest = latestJob(session);
|
|
64
84
|
var interrupted = !job && latest && latest.state === "interrupted" ? {
|
|
65
85
|
jobId: latest.id,
|
|
@@ -72,20 +92,17 @@ function attachProjectScheduledMessages(ctx) {
|
|
|
72
92
|
else ctx.sendToSession(session.localId, message);
|
|
73
93
|
return message;
|
|
74
94
|
}
|
|
75
|
-
|
|
76
95
|
function visibleError(session, targetWs, text) {
|
|
77
96
|
var message = { type: "error", text: text };
|
|
78
97
|
if (targetWs) ctx.sendTo(targetWs, message);
|
|
79
98
|
else if (session) ctx.sendToSession(session.localId, message);
|
|
80
99
|
}
|
|
81
|
-
|
|
82
100
|
function ownerFor(session) {
|
|
83
101
|
if (!users.isMultiUser()) return LOCAL_OWNER;
|
|
84
102
|
if (!session.ownerId) throw new Error("Scheduled messages require a session owner.");
|
|
85
103
|
if (!users.findUserById(session.ownerId)) throw new Error("The session owner no longer exists.");
|
|
86
104
|
return session.ownerId;
|
|
87
105
|
}
|
|
88
|
-
|
|
89
106
|
function authorize(session, actorId) {
|
|
90
107
|
if (!session || session.destroying) throw new Error("The session is no longer available.");
|
|
91
108
|
if (sm.sessions.get(session.localId) !== session) throw new Error("The session is no longer live.");
|
|
@@ -106,7 +123,6 @@ function attachProjectScheduledMessages(ctx) {
|
|
|
106
123
|
}
|
|
107
124
|
return ownerId;
|
|
108
125
|
}
|
|
109
|
-
|
|
110
126
|
function buildSpec(session, text, resetsAt, actorId, revision) {
|
|
111
127
|
var timestamp = now();
|
|
112
128
|
var runAt = resetsAt <= timestamp ? timestamp + 5000 : resetsAt + 60000;
|
|
@@ -119,6 +135,8 @@ function attachProjectScheduledMessages(ctx) {
|
|
|
119
135
|
target: { kind: "project-session", id: session.cliSessionId },
|
|
120
136
|
payload: {
|
|
121
137
|
text: text,
|
|
138
|
+
hasAttachments: false,
|
|
139
|
+
queueAdmissionRequired: true,
|
|
122
140
|
cliSessionId: session.cliSessionId,
|
|
123
141
|
actorId: actorId || LOCAL_OWNER,
|
|
124
142
|
sessionOwnerId: ownerId,
|
|
@@ -129,27 +147,117 @@ function attachProjectScheduledMessages(ctx) {
|
|
|
129
147
|
runAt: runAt,
|
|
130
148
|
};
|
|
131
149
|
}
|
|
150
|
+
function requestJobs(session, actorId, requestId) {
|
|
151
|
+
if (!scheduler || !session || !requestId) return [];
|
|
152
|
+
var jobs = scheduler.listJobs();
|
|
153
|
+
return jobs.filter(function (job) {
|
|
154
|
+
return job.type === type && job.projectId === slug && job.target && job.target.id === session.cliSessionId &&
|
|
155
|
+
job.payload && job.payload.admissionRequestId === requestId && job.payload.actorId === actorId;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function requestIdempotencyKey(session, actorId, requestId) {
|
|
159
|
+
var base = "project-message:" + slug + ":" + session.cliSessionId + ":actor:" + actorId + ":request:" + requestId;
|
|
160
|
+
var jobs = requestJobs(session, actorId, requestId);
|
|
161
|
+
var retryCount = 0;
|
|
162
|
+
for (var i = 0; i < jobs.length; i++) if (jobs[i].state === "cancelled" && jobs[i].cancelledReason === "queue_admission_failed") retryCount++;
|
|
163
|
+
var key = crypto.createHash("sha256").update(base).digest("hex");
|
|
164
|
+
return "project-message-request:" + key + (retryCount ? ":retry-" + retryCount : "");
|
|
165
|
+
}
|
|
166
|
+
function sendAdmissionResult(targetWs, session, actorId, requestId, job, error, transportAccountId) {
|
|
167
|
+
if (!targetWs || !requestId) return;
|
|
168
|
+
ctx.sendTo(targetWs, {
|
|
169
|
+
type: "schedule_message_result",
|
|
170
|
+
requestId: requestId,
|
|
171
|
+
projectSlug: slug,
|
|
172
|
+
sessionId: session ? session.localId : null,
|
|
173
|
+
accountId: transportAccountId || actorId || "default",
|
|
174
|
+
ok: !error,
|
|
175
|
+
error: error || null,
|
|
176
|
+
scheduledMessage: job ? projection(job, queueItemForJob(session, job, actorId)) : null,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
132
179
|
|
|
133
|
-
function schedule(session, text, resetsAt, actorUser, targetWs) {
|
|
180
|
+
function schedule(session, text, resetsAt, actorUser, targetWs, attachments, requestId, transportAccountId) {
|
|
181
|
+
var actorId = users.isMultiUser() ? (actorUser && actorUser.id) : LOCAL_OWNER;
|
|
134
182
|
if (!scheduler || stopped) {
|
|
183
|
+
sendAdmissionResult(targetWs, session, actorId, requestId, null, "Scheduled messages are temporarily unavailable.", transportAccountId);
|
|
135
184
|
visibleError(session, targetWs, "Scheduled messages are temporarily unavailable.");
|
|
136
185
|
return null;
|
|
137
186
|
}
|
|
138
|
-
|
|
187
|
+
var images = attachments && Array.isArray(attachments.images) ? attachments.images : [];
|
|
188
|
+
var pastes = attachments && Array.isArray(attachments.pastes) ? attachments.pastes : [];
|
|
189
|
+
if (attachments && Object.prototype.hasOwnProperty.call(attachments, "images") && !Array.isArray(attachments.images)) {
|
|
190
|
+
sendAdmissionResult(targetWs, session, actorId, requestId, null, "Invalid image attachments.", transportAccountId);
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
if (attachments && Object.prototype.hasOwnProperty.call(attachments, "pastes") && !Array.isArray(attachments.pastes)) {
|
|
194
|
+
sendAdmissionResult(targetWs, session, actorId, requestId, null, "Invalid paste attachments.", transportAccountId);
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
if (requestId && (typeof requestId !== "string" || !REQUEST_ID_RE.test(requestId))) {
|
|
198
|
+
sendAdmissionResult(targetWs, session, actorId, requestId, null, "Invalid schedule request identifier.", transportAccountId);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
if (typeof text !== "string" || (!text.trim() && !images.length && !pastes.length) || !isFinite(Number(resetsAt))) {
|
|
202
|
+
sendAdmissionResult(targetWs, session, actorId, requestId, null, "A valid scheduled message and reset time are required.", transportAccountId);
|
|
139
203
|
visibleError(session, targetWs, "A valid scheduled message and reset time are required.");
|
|
140
204
|
return null;
|
|
141
205
|
}
|
|
142
206
|
try {
|
|
143
|
-
|
|
207
|
+
// Authorization is deliberately performed before looking up a durable
|
|
208
|
+
// request association, so replay cannot disclose another account's job.
|
|
209
|
+
authorize(session, actorId);
|
|
210
|
+
var priorJobs = requestJobs(session, actorId, requestId);
|
|
211
|
+
var prior = priorJobs.length ? priorJobs[priorJobs.length - 1] : null;
|
|
212
|
+
if (prior && (prior.state !== "cancelled" || prior.cancelledReason !== "queue_admission_failed")) {
|
|
213
|
+
if (prior.state === "queued") {
|
|
214
|
+
var priorItem = queueItemForJob(session, prior, actorId);
|
|
215
|
+
if (!priorItem) throw new Error("Scheduled queue item is missing; refusing to replay request.");
|
|
216
|
+
if (["cancelled", "consumed", "failed"].indexOf(priorItem.state) >= 0) throw new Error("Scheduled queue item is no longer available; refusing to replay request.");
|
|
217
|
+
}
|
|
218
|
+
sendAdmissionResult(targetWs, session, actorId, requestId, prior, null, transportAccountId);
|
|
219
|
+
return prior;
|
|
220
|
+
}
|
|
144
221
|
var revision = makeRevision();
|
|
145
222
|
var spec = buildSpec(session, text, Number(resetsAt), actorId, revision);
|
|
223
|
+
if (requestId) {
|
|
224
|
+
spec.idempotencyKey = requestIdempotencyKey(session, actorId, requestId);
|
|
225
|
+
spec.payload.admissionRequestId = requestId;
|
|
226
|
+
spec.payload.admissionAccountId = actorId;
|
|
227
|
+
}
|
|
228
|
+
spec.payload.hasAttachments = images.length > 0 || pastes.length > 0;
|
|
146
229
|
var current = pendingJob(session);
|
|
230
|
+
if (current && requestId && (!current.payload || current.payload.admissionRequestId !== requestId)) {
|
|
231
|
+
throw new Error("Another scheduled message is already pending.");
|
|
232
|
+
}
|
|
233
|
+
if (current && pendingMessageQueue) validateQueueControl(session, current, actorId, null, false);
|
|
147
234
|
var job = current ? scheduler.replaceQueued(current.id, spec) : scheduler.enqueue(spec);
|
|
148
|
-
|
|
235
|
+
if (pendingMessageQueue && job) {
|
|
236
|
+
var queuedScheduled = pendingMessageQueue.upsertScheduled(session, {
|
|
237
|
+
type: "message",
|
|
238
|
+
text: text,
|
|
239
|
+
images: images,
|
|
240
|
+
pastes: pastes,
|
|
241
|
+
clientMessageId: requestId ? "scheduled-request-" + requestId : "scheduled-" + job.id,
|
|
242
|
+
schedule: { jobId: job.id, revision: job.payload.revision, notBefore: job.runAt, ready: false },
|
|
243
|
+
}, queueActor(actorId));
|
|
244
|
+
if (!queuedScheduled.ok) {
|
|
245
|
+
if (current) {
|
|
246
|
+
try { scheduler.replaceQueued(current.id, current); }
|
|
247
|
+
catch (rollbackError) { throw new Error((queuedScheduled.error || "Unable to queue scheduled message") + "; scheduler rollback failed: " + rollbackError.message); }
|
|
248
|
+
} else {
|
|
249
|
+
scheduler.cancel(job.id, "queue_admission_failed");
|
|
250
|
+
}
|
|
251
|
+
throw new Error(queuedScheduled.error || "Unable to queue scheduled message");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
session.scheduledMessage = projection(job, queueItemForJob(session, job, job && job.payload && job.payload.actorId));
|
|
255
|
+
sendAdmissionResult(targetWs, session, actorId, requestId, job, null, transportAccountId);
|
|
149
256
|
sm.sendAndRecord(session, Object.assign({ type: "scheduled_message_queued" }, session.scheduledMessage));
|
|
150
257
|
return job;
|
|
151
258
|
} catch (error) {
|
|
152
259
|
if (session) session.rateLimitAutoContinuePending = false;
|
|
260
|
+
sendAdmissionResult(targetWs, session, actorId, requestId, null, error.message, transportAccountId);
|
|
153
261
|
visibleError(session, targetWs, "Could not schedule the message: " + error.message);
|
|
154
262
|
return null;
|
|
155
263
|
}
|
|
@@ -157,7 +265,7 @@ function attachProjectScheduledMessages(ctx) {
|
|
|
157
265
|
|
|
158
266
|
function requireCorrelation(job, correlation, targetWs) {
|
|
159
267
|
if (!targetWs) return;
|
|
160
|
-
if (!correlation || correlation.jobId !== job.id || correlation.revision !== job.payload.revision) {
|
|
268
|
+
if (!correlation || correlation.jobId !== job.id || correlation.revision !== job.payload.revision || correlation.queueRevision == null || !pendingMessageQueue || Number(correlation.queueRevision) !== pendingMessageQueue.getRevision()) {
|
|
161
269
|
throw new Error("The scheduled-message control is stale.");
|
|
162
270
|
}
|
|
163
271
|
}
|
|
@@ -165,13 +273,18 @@ function attachProjectScheduledMessages(ctx) {
|
|
|
165
273
|
function cancel(session, actorUser, targetWs, reason, correlation) {
|
|
166
274
|
try {
|
|
167
275
|
authorize(session, users.isMultiUser() ? (actorUser && actorUser.id) : LOCAL_OWNER);
|
|
168
|
-
var job = pendingJob(session);
|
|
276
|
+
var job = pendingJob(session) || latestJob(session);
|
|
169
277
|
if (!job) return false;
|
|
170
278
|
requireCorrelation(job, correlation, targetWs);
|
|
171
|
-
|
|
279
|
+
if (pendingMessageQueue) validateQueueControl(session, job, actorUser && actorUser.id, correlation, !!targetWs);
|
|
280
|
+
if (pendingMessageQueue) {
|
|
281
|
+
var queueCancelled = pendingMessageQueue.cancelScheduled(session, job.id, queueActor(actorUser && actorUser.id));
|
|
282
|
+
if (!queueCancelled || queueCancelled.ok !== true) throw new Error((queueCancelled && queueCancelled.error) || "The scheduled queue item could not be cancelled.");
|
|
283
|
+
}
|
|
284
|
+
if (job.state === "queued" && !pendingMessageQueue) scheduler.cancel(job.id, reason || "cancelled_by_user");
|
|
172
285
|
session.scheduledMessage = null;
|
|
173
286
|
session.rateLimitAutoContinuePending = false;
|
|
174
|
-
sm.sendAndRecord(session, { type: "scheduled_message_cancelled", jobId: job.id, revision: job.payload.revision });
|
|
287
|
+
if (!pendingMessageQueue) sm.sendAndRecord(session, { type: "scheduled_message_cancelled", jobId: job.id, revision: job.payload.revision });
|
|
175
288
|
return true;
|
|
176
289
|
} catch (error) {
|
|
177
290
|
visibleError(session, targetWs, "Could not cancel the scheduled message: " + error.message);
|
|
@@ -185,11 +298,27 @@ function attachProjectScheduledMessages(ctx) {
|
|
|
185
298
|
var job = pendingJob(session);
|
|
186
299
|
if (!job) return false;
|
|
187
300
|
requireCorrelation(job, correlation, targetWs);
|
|
301
|
+
var existing = validateQueueControl(session, job, actorUser && actorUser.id, correlation, !!targetWs);
|
|
188
302
|
var updated = scheduler.replaceQueued(job.id, {
|
|
189
303
|
runAt: now(),
|
|
190
304
|
idempotencyKey: job.idempotencyKey + ":now",
|
|
191
305
|
});
|
|
192
|
-
|
|
306
|
+
if (pendingMessageQueue) {
|
|
307
|
+
var queued = pendingMessageQueue.upsertScheduled(session, {
|
|
308
|
+
type: "message",
|
|
309
|
+
text: existing ? existing.message.text : updated.payload.text,
|
|
310
|
+
images: existing ? existing.message.images : undefined,
|
|
311
|
+
pastes: existing ? existing.message.pastes : undefined,
|
|
312
|
+
clientMessageId: existing ? existing.clientMessageId : "scheduled-" + updated.id,
|
|
313
|
+
schedule: { jobId: updated.id, revision: updated.payload.revision, notBefore: updated.runAt, ready: false },
|
|
314
|
+
}, queueActor(actorUser && actorUser.id));
|
|
315
|
+
if (!queued.ok) {
|
|
316
|
+
try { scheduler.replaceQueued(job.id, job); }
|
|
317
|
+
catch (rollbackError) { throw new Error((queued.error || "The scheduled queue item could not be updated.") + "; scheduler rollback failed: " + rollbackError.message); }
|
|
318
|
+
throw new Error(queued.error || "The scheduled queue item could not be updated.");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
session.scheduledMessage = projection(updated, existing);
|
|
193
322
|
scheduler.tick();
|
|
194
323
|
return true;
|
|
195
324
|
} catch (error) {
|
|
@@ -237,43 +366,94 @@ function attachProjectScheduledMessages(ctx) {
|
|
|
237
366
|
var linuxUser = ctx.ensureProjectAccessForSession(session);
|
|
238
367
|
if (ctx.osUsers && !linuxUser) throw new Error("The session owner OS identity is unavailable.");
|
|
239
368
|
execution.recordMetadata({ projectSlug: slug, cliSessionId: session.cliSessionId, revision: job.payload.revision });
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
text: job.payload.text,
|
|
245
|
-
_ts: now(),
|
|
246
|
-
scheduledJobId: job.id,
|
|
247
|
-
scheduledRevision: job.payload.revision,
|
|
248
|
-
};
|
|
249
|
-
session.history.push(userMessage);
|
|
250
|
-
sm.appendToSessionFile(session, userMessage);
|
|
251
|
-
ctx.sendToSession(session.localId, userMessage);
|
|
252
|
-
processingOwner = {};
|
|
253
|
-
session._scheduledMessageProcessing = processingOwner;
|
|
254
|
-
session.isProcessing = true;
|
|
255
|
-
ctx.onProcessingChanged();
|
|
256
|
-
ctx.sendToSession(session.localId, { type: "status", status: "processing" });
|
|
257
|
-
var queryPromise = sdk.startQuery(session, job.payload.text, null, linuxUser);
|
|
258
|
-
processingOwner.queryGeneration = session._queryGeneration;
|
|
259
|
-
processingOwner.queryInstance = session.queryInstance;
|
|
260
|
-
if (session.scheduledMessage && session.scheduledMessage.jobId === job.id) {
|
|
261
|
-
session.scheduledMessage = null;
|
|
262
|
-
session.rateLimitAutoContinuePending = false;
|
|
369
|
+
if (!pendingMessageQueue) throw new Error("Scheduled message queue is unavailable.");
|
|
370
|
+
var existing = queueItemForJob(session, job, job.payload.actorId);
|
|
371
|
+
if (existing && ["cancelled", "consumed", "failed"].indexOf(existing.state) >= 0) {
|
|
372
|
+
return { released: false, queueItemId: existing.id, cliSessionId: session.cliSessionId, revision: job.payload.revision };
|
|
263
373
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
374
|
+
if (!existing && ["interrupted", "completed"].indexOf(job.state) >= 0) {
|
|
375
|
+
return { released: false, queueItemId: null, cliSessionId: session.cliSessionId, revision: job.payload.revision };
|
|
376
|
+
}
|
|
377
|
+
var ensured = existing;
|
|
378
|
+
if (!ensured && job.payload.queueAdmissionRequired === true) throw new Error("Scheduled queue item is missing; refusing to recreate message payload.");
|
|
379
|
+
if (!ensured) ensured = pendingMessageQueue.upsertScheduled(session, {
|
|
380
|
+
type: "message", text: job.payload.text, clientMessageId: "scheduled-" + job.id,
|
|
381
|
+
schedule: { jobId: job.id, revision: job.payload.revision, notBefore: job.runAt, ready: false },
|
|
382
|
+
}, queueActor(job.payload.actorId));
|
|
383
|
+
if (!ensured || ensured.ok === false) throw new Error(ensured.error || "Scheduled queue handoff could not be prepared.");
|
|
384
|
+
var released = pendingMessageQueue.releaseScheduled(session, job.id, job.payload.revision);
|
|
385
|
+
if (!released) throw new Error("Scheduled message queue handoff could not be released.");
|
|
386
|
+
if (typeof consumePendingMessage === "function") setImmediate(function () { consumePendingMessage(session); });
|
|
387
|
+
return { released: true, queueItemId: released.id, cliSessionId: session.cliSessionId, revision: job.payload.revision };
|
|
269
388
|
} catch (error) {
|
|
270
389
|
failJob(session, job, error, processingOwner);
|
|
271
390
|
throw error;
|
|
272
391
|
}
|
|
273
392
|
}
|
|
274
393
|
|
|
394
|
+
function onQueueConsumed(item) {
|
|
395
|
+
if (!item || !item.schedule || !scheduler) return false;
|
|
396
|
+
var job = scheduler.getJob(item.schedule.jobId);
|
|
397
|
+
var session = job && job.target ? findSession(job.target.id) : null;
|
|
398
|
+
if (!session) return false;
|
|
399
|
+
if (session.scheduledMessage && session.scheduledMessage.jobId === item.schedule.jobId) {
|
|
400
|
+
session.scheduledMessage = null;
|
|
401
|
+
session.rateLimitAutoContinuePending = false;
|
|
402
|
+
}
|
|
403
|
+
sm.sendAndRecord(session, { type: "scheduled_message_sent", jobId: job.id, revision: item.schedule.revision });
|
|
404
|
+
sm.broadcastSessionList();
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function onQueueCancelled(item) {
|
|
409
|
+
if (!item || !item.schedule || !scheduler) return false;
|
|
410
|
+
var job = scheduler.getJob(item.schedule.jobId);
|
|
411
|
+
if (job && job.state === "queued") {
|
|
412
|
+
try { scheduler.cancel(job.id, "cancelled_by_user"); } catch (error) {}
|
|
413
|
+
}
|
|
414
|
+
var session = job && job.target ? findSession(job.target.id) : null;
|
|
415
|
+
if (session && session.scheduledMessage && session.scheduledMessage.jobId === item.schedule.jobId) {
|
|
416
|
+
session.scheduledMessage = null;
|
|
417
|
+
session.rateLimitAutoContinuePending = false;
|
|
418
|
+
sm.sendAndRecord(session, { type: "scheduled_message_cancelled", jobId: item.schedule.jobId, revision: item.schedule.revision });
|
|
419
|
+
}
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function authorizePending(item, session) {
|
|
424
|
+
if (!item || !item.schedule || !scheduler || !session) return true;
|
|
425
|
+
var job = scheduler.getJob(item.schedule.jobId);
|
|
426
|
+
var expectedActorId = users.isMultiUser() ? job && job.payload && job.payload.actorId : "default";
|
|
427
|
+
if (!job || ["running", "completed", "interrupted"].indexOf(job.state) < 0 || !job.target || job.target.id !== session.cliSessionId ||
|
|
428
|
+
!job.payload || job.projectId !== slug || job.payload.revision !== item.schedule.revision || item.actorId !== expectedActorId ||
|
|
429
|
+
job.ownerId !== job.payload.sessionOwnerId) return false;
|
|
430
|
+
try { return authorize(session, job.payload.actorId) === job.ownerId; } catch (error) { return false; }
|
|
431
|
+
}
|
|
432
|
+
|
|
275
433
|
function rehydrate() {
|
|
276
|
-
sm.sessions.forEach(function (session) {
|
|
434
|
+
sm.sessions.forEach(function (session) {
|
|
435
|
+
var pending = pendingJob(session);
|
|
436
|
+
session.scheduledMessage = projection(pending, queueItemForJob(session, pending, pending && pending.payload && pending.payload.actorId));
|
|
437
|
+
if (!pendingMessageQueue) return;
|
|
438
|
+
var jobs = scheduler.listJobs();
|
|
439
|
+
for (var i = 0; i < jobs.length; i++) {
|
|
440
|
+
if (jobs[i].type !== type || jobs[i].projectId !== slug || !jobs[i].target || jobs[i].target.id !== session.cliSessionId) continue;
|
|
441
|
+
var actor = users.isMultiUser() ? users.findUserById(jobs[i].payload.actorId) : { id: "default" };
|
|
442
|
+
if (jobs[i].state === "queued" && !queueItemForJob(session, jobs[i], jobs[i].payload.actorId)) {
|
|
443
|
+
if (jobs[i].payload.queueAdmissionRequired !== true) pendingMessageQueue.upsertScheduled(session, {
|
|
444
|
+
type: "message", text: jobs[i].payload.text, clientMessageId: "scheduled-" + jobs[i].id,
|
|
445
|
+
schedule: { jobId: jobs[i].id, revision: jobs[i].payload.revision, notBefore: jobs[i].runAt, ready: false },
|
|
446
|
+
}, actor);
|
|
447
|
+
}
|
|
448
|
+
else if (["interrupted", "completed"].indexOf(jobs[i].state) >= 0) {
|
|
449
|
+
var existing = queueItemForJob(session, jobs[i], jobs[i].payload.actorId);
|
|
450
|
+
if (existing && existing.state === "pending" && jobs[i].runAt <= now() && authorizePending(existing, session)) {
|
|
451
|
+
var released = pendingMessageQueue.releaseScheduled(session, jobs[i].id, jobs[i].payload.revision);
|
|
452
|
+
if (released && typeof consumePendingMessage === "function") setImmediate(function () { consumePendingMessage(session); });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
});
|
|
277
457
|
}
|
|
278
458
|
|
|
279
459
|
function deleted(localId, session) {
|
|
@@ -310,6 +490,9 @@ function attachProjectScheduledMessages(ctx) {
|
|
|
310
490
|
notify: notify,
|
|
311
491
|
shutdown: shutdown,
|
|
312
492
|
handlerType: type,
|
|
493
|
+
authorizePending: authorizePending,
|
|
494
|
+
onQueueConsumed: onQueueConsumed,
|
|
495
|
+
onQueueCancelled: onQueueCancelled,
|
|
313
496
|
};
|
|
314
497
|
}
|
|
315
498
|
|
|
@@ -303,22 +303,42 @@ function attachUserMessage(ctx) {
|
|
|
303
303
|
// --- Schedule message for after rate limit resets ---
|
|
304
304
|
if (msg.type === "schedule_message") {
|
|
305
305
|
var schedSession = getSessionForWs(ws);
|
|
306
|
-
|
|
307
|
-
|
|
306
|
+
var transportAccountId = (ws && ws._clayUser && ws._clayUser.id) ? String(ws._clayUser.id) : "default";
|
|
307
|
+
if (!schedSession || msg.projectSlug !== slug || String(msg.sessionId) !== String(schedSession && schedSession.localId) ||
|
|
308
|
+
(msg.accountId || "default") !== transportAccountId) {
|
|
309
|
+
if (msg.requestId) sendTo(ws, { type: "schedule_message_result", requestId: msg.requestId,
|
|
310
|
+
projectSlug: msg.projectSlug || slug, sessionId: msg.sessionId == null ? null : msg.sessionId,
|
|
311
|
+
accountId: transportAccountId, ok: false, error: "The scheduling session or account is stale.", scheduledMessage: null });
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
scheduleMessage(schedSession, msg.text || "", msg.resetsAt, ws._clayUser || null, ws,
|
|
315
|
+
{ images: msg.images, pastes: msg.pastes }, msg.requestId || null, transportAccountId);
|
|
308
316
|
return true;
|
|
309
317
|
}
|
|
310
318
|
|
|
311
319
|
if (msg.type === "cancel_scheduled_message") {
|
|
312
320
|
var cancelSession = getSessionForWs(ws);
|
|
313
321
|
if (!cancelSession) return true;
|
|
314
|
-
|
|
322
|
+
if (msg.projectSlug && msg.projectSlug !== slug) return true;
|
|
323
|
+
if (msg.sessionId != null && String(msg.sessionId) !== String(cancelSession.localId)) return true;
|
|
324
|
+
cancelScheduledMessage(cancelSession, ws._clayUser || null, ws, { jobId: msg.jobId, revision: msg.revision, queueRevision: msg.queueRevision, requestId: msg.requestId });
|
|
315
325
|
return true;
|
|
316
326
|
}
|
|
317
327
|
|
|
318
328
|
if (msg.type === "send_scheduled_now") {
|
|
319
329
|
var nowSession = getSessionForWs(ws);
|
|
320
|
-
|
|
321
|
-
|
|
330
|
+
var requestedContext = { projectSlug: slug, sessionId: nowSession ? nowSession.localId : msg.sessionId, revision: pendingMessageQueue ? pendingMessageQueue.getRevision() : 0, items: [], paused: false };
|
|
331
|
+
var controlResult;
|
|
332
|
+
if (!nowSession) controlResult = Object.assign({}, requestedContext, { ok: false, error: "The session is no longer available." });
|
|
333
|
+
else if (msg.projectSlug && msg.projectSlug !== slug) controlResult = Object.assign({}, requestedContext, { ok: false, error: "Project changed" });
|
|
334
|
+
else if (msg.sessionId != null && String(msg.sessionId) !== String(nowSession.localId)) controlResult = Object.assign({}, requestedContext, { ok: false, error: "Session changed" });
|
|
335
|
+
else if (!nowSession.scheduledMessage) controlResult = Object.assign({}, requestedContext, { ok: false, error: "The scheduled message is no longer available." });
|
|
336
|
+
else {
|
|
337
|
+
var sentNow = sendScheduledMessageNow(nowSession, ws._clayUser || null, ws, { jobId: msg.jobId, revision: msg.revision, queueRevision: msg.queueRevision, requestId: msg.requestId });
|
|
338
|
+
controlResult = pendingMessageQueue ? pendingMessageQueue.hydrate(nowSession, ws._clayUser || null) : { ok: sentNow === true, projectSlug: slug, sessionId: nowSession.localId, revision: 0, items: [], paused: false };
|
|
339
|
+
if (sentNow !== true) controlResult = Object.assign({}, controlResult, { ok: false, error: "Could not send the scheduled message." });
|
|
340
|
+
}
|
|
341
|
+
if (msg.requestId) sendTo(ws, { type: "pending_message_result", requestId: msg.requestId, result: controlResult });
|
|
322
342
|
return true;
|
|
323
343
|
}
|
|
324
344
|
|