clay-server 4.1.0-beta.8 → 4.1.0
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/context-usage.js +47 -0
- package/lib/issues-mcp-server.js +1 -1
- package/lib/issues-service.js +3 -1
- package/lib/loop-registry-occurrence.js +1 -0
- package/lib/notes.js +20 -3
- package/lib/os-users.js +6 -0
- package/lib/project-connection.js +14 -3
- package/lib/project-file-path.js +13 -0
- package/lib/project-file-watch.js +19 -8
- package/lib/project-filesystem.js +10 -16
- package/lib/project-http.js +53 -48
- package/lib/project-logs-mcp-server.js +8 -8
- package/lib/project-logs-schema.js +1 -1
- package/lib/project-logs-service.js +46 -13
- package/lib/project-logs-store.js +17 -15
- package/lib/project-logs.js +70 -0
- package/lib/project-loop.js +1 -0
- package/lib/project-notifications.js +1 -1
- package/lib/project-pending-message-queue.js +71 -7
- package/lib/project-scheduled-messages.js +235 -52
- package/lib/project-scheduled-result-store.js +131 -0
- package/lib/project-scheduled-task-execution.js +35 -2
- package/lib/project-session-notes.js +11 -4
- package/lib/project-sessions.js +7 -0
- package/lib/project-user-message.js +72 -5
- package/lib/project.js +60 -5
- package/lib/public/app.js +27 -12
- package/lib/public/css/avatar-imprints.css +1 -1
- package/lib/public/css/command-palette.css +20 -0
- package/lib/public/css/git-panel.css +13 -12
- package/lib/public/css/git-placard.css +4 -4
- package/lib/public/css/input.css +0 -109
- package/lib/public/css/issues.css +78 -31
- package/lib/public/css/mobile-nav.css +7 -0
- package/lib/public/css/notifications-center.css +3 -3
- package/lib/public/css/pending-message-queue.css +3 -0
- package/lib/public/css/project-logs.css +15 -0
- package/lib/public/css/sidebar.css +7 -0
- package/lib/public/css/sticky-notes.css +29 -0
- package/lib/public/modules/app-connection.js +9 -1
- package/lib/public/modules/app-favicon.js +10 -0
- package/lib/public/modules/app-messages.js +49 -37
- package/lib/public/modules/app-notifications.js +2 -0
- package/lib/public/modules/app-panels.js +77 -23
- package/lib/public/modules/app-projects.js +3 -0
- package/lib/public/modules/app-rate-limit.js +148 -243
- package/lib/public/modules/avatar-imprint.js +25 -70
- package/lib/public/modules/avatar.js +12 -4
- package/lib/public/modules/chat-render-runtime.js +20 -2
- package/lib/public/modules/clay-issue-links.js +23 -2
- package/lib/public/modules/clay-log-links.js +11 -1
- package/lib/public/modules/context-view-preference.js +143 -0
- package/lib/public/modules/default-vendor.js +116 -0
- package/lib/public/modules/filebrowser-tabs.js +1 -0
- package/lib/public/modules/filebrowser.js +45 -18
- package/lib/public/modules/input.js +81 -20
- package/lib/public/modules/issues-render.js +36 -10
- package/lib/public/modules/issues.js +95 -13
- package/lib/public/modules/markdown.js +3 -1
- package/lib/public/modules/pane-bridge.js +14 -8
- package/lib/public/modules/pane-issue-bridge.js +27 -0
- package/lib/public/modules/pending-message-queue.js +43 -1
- package/lib/public/modules/permission-hints.js +13 -0
- package/lib/public/modules/profile.js +9 -9
- package/lib/public/modules/project-activation.js +49 -0
- package/lib/public/modules/project-logs-render.js +13 -3
- package/lib/public/modules/project-logs.js +45 -15
- package/lib/public/modules/right-workbench.js +44 -0
- package/lib/public/modules/scheduled-message-state.js +12 -36
- package/lib/public/modules/scheduled-result-client.js +33 -0
- package/lib/public/modules/scheduled-result-display.js +50 -0
- package/lib/public/modules/scheduled-result-notification.js +31 -0
- package/lib/public/modules/scheduled-task-presentation.js +15 -0
- package/lib/public/modules/scheduled-tasks.js +9 -21
- package/lib/public/modules/sidebar-mobile.js +51 -3
- package/lib/public/modules/sidebar-sessions.js +65 -5
- package/lib/public/modules/split-session-boundary.js +28 -0
- package/lib/public/modules/split-view.js +42 -7
- package/lib/public/modules/sticky-notes-browser.js +130 -18
- package/lib/public/modules/sticky-notes.js +2 -3
- package/lib/public/modules/terminal.js +4 -4
- package/lib/public/modules/thinking-lifecycle.js +4 -1
- package/lib/public/modules/tools.js +5 -1
- package/lib/scheduled-run-state.js +10 -2
- package/lib/scheduler.js +1 -0
- package/lib/sdk-bridge.js +97 -61
- package/lib/sdk-message-processor.js +44 -13
- package/lib/sdk-skill-discovery.js +6 -28
- package/lib/sdk-turn-queue.js +14 -0
- package/lib/server-context-view.js +53 -0
- package/lib/server-default-vendor.js +60 -0
- package/lib/server.js +23 -6
- package/lib/session-notes-mcp-server.js +3 -5
- package/lib/session-pair-factory.js +2 -2
- package/lib/session-pair-mcp-server.js +5 -4
- package/lib/session-title-generator.js +89 -0
- package/lib/session-title-policy.js +8 -2
- package/lib/sessions.js +26 -1
- package/lib/user-presence.js +6 -0
- package/lib/users-context-view-preferences.js +65 -0
- package/lib/users-default-vendor-preferences.js +80 -0
- package/lib/users.js +12 -0
- package/lib/ws-schema.js +11 -0
- package/lib/yoke/acp-agent-profiles.js +15 -2
- package/lib/yoke/adapters/acp.js +3 -2
- package/lib/yoke/adapters/antigravity.js +2 -2
- package/lib/yoke/adapters/claude-worker.js +11 -12
- package/lib/yoke/adapters/claude.js +75 -22
- package/lib/yoke/adapters/codex.js +216 -29
- package/lib/yoke/adapters/kiro.js +6 -5
- package/lib/yoke/claude-context-usage.js +57 -0
- package/lib/yoke/claude-message-correlation.js +31 -0
- package/lib/yoke/claude-model-switch.js +15 -0
- package/lib/yoke/index.js +5 -1
- package/lib/yoke/mcp-bridge-server.js +18 -5
- package/lib/yoke/session-skill-catalog.js +45 -0
- package/lib/yoke/skill-discovery.js +60 -20
- package/lib/yoke/vendor-registry.js +1 -1
- package/package.json +5 -7
|
@@ -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
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Server-owned Scheduled Tasks results inside the Project Logs record stream.
|
|
2
|
+
// This capability is never exposed through the human or agent write surfaces.
|
|
3
|
+
|
|
4
|
+
var crypto = require("crypto");
|
|
5
|
+
var recordStore = require("./knowledge-record-store");
|
|
6
|
+
var logsSchema = require("./project-logs-schema");
|
|
7
|
+
var logsSnapshot = require("./project-logs-snapshot");
|
|
8
|
+
|
|
9
|
+
var ID_PATTERN = /^[A-Za-z0-9_-]{1,200}$/;
|
|
10
|
+
var OUTCOMES = ["completed", "failed", "needs-input", "interrupted", "skipped-busy"];
|
|
11
|
+
|
|
12
|
+
function clean(value, max) {
|
|
13
|
+
return logsSchema.cleanLine(typeof value === "string" ? value : "", max);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeMetadata(value) {
|
|
17
|
+
var source = value || {};
|
|
18
|
+
if (!ID_PATTERN.test(source.runId || "") || !ID_PATTERN.test(source.scheduleId || "")) return null;
|
|
19
|
+
if (OUTCOMES.indexOf(source.outcome) === -1) return null;
|
|
20
|
+
return {
|
|
21
|
+
runId: source.runId,
|
|
22
|
+
scheduleId: source.scheduleId,
|
|
23
|
+
outcome: source.outcome,
|
|
24
|
+
ownerId: source.ownerId ? clean(String(source.ownerId), 120) : null,
|
|
25
|
+
driverOriginId: clean(source.driverOriginId || "", 200) || null,
|
|
26
|
+
workerOriginId: clean(source.workerOriginId || "", 200) || null,
|
|
27
|
+
driverProviderSessionId: clean(source.driverProviderSessionId || "", 200) || null,
|
|
28
|
+
workerProviderSessionId: clean(source.workerProviderSessionId || "", 200) || null,
|
|
29
|
+
startedAt: Number(source.startedAt) || null,
|
|
30
|
+
finishedAt: Number(source.finishedAt) || null,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function visibleTo(metadata, viewerId) {
|
|
35
|
+
if (!metadata || viewerId === undefined) return true;
|
|
36
|
+
return (metadata.ownerId || null) === (viewerId || null);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function outcomeLabel(outcome) {
|
|
40
|
+
if (outcome === "completed") return "Completed";
|
|
41
|
+
if (outcome === "needs-input") return "Needs input";
|
|
42
|
+
if (outcome === "interrupted") return "Interrupted";
|
|
43
|
+
if (outcome === "skipped-busy") return "Skipped";
|
|
44
|
+
return "Failed";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function fallbackSummary(outcome) {
|
|
48
|
+
if (outcome === "completed") return "The scheduled task completed successfully.";
|
|
49
|
+
if (outcome === "needs-input") return "The scheduled task is waiting for user input or permission.";
|
|
50
|
+
if (outcome === "interrupted") return "The scheduled task was interrupted before completion.";
|
|
51
|
+
if (outcome === "skipped-busy") return "The scheduled occurrence was skipped because an earlier run was still active.";
|
|
52
|
+
return "The scheduled task failed.";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function rootId(scopeId, runId) {
|
|
56
|
+
var digest = crypto.createHash("sha256").update(scopeId + "\u0000scheduled-result\u0000" + runId).digest("base64url");
|
|
57
|
+
return "sresult-" + digest.substring(0, 24);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function attachScheduledResultStore(ctx) {
|
|
61
|
+
function resultRecord(runId) {
|
|
62
|
+
var records = ctx.store.all();
|
|
63
|
+
for (var i = 0; i < records.length; i++) {
|
|
64
|
+
if (records[i].op === "create" && records[i].scheduledResult && records[i].scheduledResult.runId === runId) return records[i];
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function create(input, context) {
|
|
70
|
+
var data = input || {};
|
|
71
|
+
var metadata = normalizeMetadata(data);
|
|
72
|
+
if (!metadata) throw new Error("Scheduled result identity is invalid.");
|
|
73
|
+
var existing = resultRecord(metadata.runId);
|
|
74
|
+
if (existing) return { entry: ctx.read(ctx.refFor(existing.rootId || existing.id), false), created: false };
|
|
75
|
+
var name = clean(data.name || "Scheduled task", 160) || "Scheduled task";
|
|
76
|
+
var reason = clean(data.summary || data.reason || "", 400);
|
|
77
|
+
var summary = reason || fallbackSummary(metadata.outcome);
|
|
78
|
+
var title = name + " · " + outcomeLabel(metadata.outcome);
|
|
79
|
+
var body = "## Scheduled result\n\n" + summary + "\n\n" +
|
|
80
|
+
"- Outcome: " + outcomeLabel(metadata.outcome) + "\n" +
|
|
81
|
+
"- Run: `" + metadata.runId + "`\n" +
|
|
82
|
+
(metadata.startedAt ? "- Started: " + new Date(metadata.startedAt).toISOString() + "\n" : "") +
|
|
83
|
+
(metadata.finishedAt ? "- Finished: " + new Date(metadata.finishedAt).toISOString() + "\n" : "");
|
|
84
|
+
var id = rootId(ctx.scopeId, metadata.runId);
|
|
85
|
+
var snapshot = logsSnapshot.cloneSnapshot({ category: "scheduled-result", priority: metadata.outcome === "failed" ? "important" : "normal", title: title, summary: summary, body: body, tags: ["scheduled-task", metadata.outcome], links: [] });
|
|
86
|
+
ctx.store.append({
|
|
87
|
+
id: id, rootId: id, op: "create", scope: ctx.scopeId,
|
|
88
|
+
kind: snapshot.category, priority: snapshot.priority, title: snapshot.title,
|
|
89
|
+
summary: snapshot.summary, body: snapshot.body, tags: snapshot.tags,
|
|
90
|
+
links: snapshot.links, snapshot: snapshot,
|
|
91
|
+
author: logsSchema.normalizeAuthor({ type: "system", displayName: "Scheduled Tasks" }),
|
|
92
|
+
context: context || null, scheduledResult: metadata, at: metadata.finishedAt || Date.now(),
|
|
93
|
+
});
|
|
94
|
+
return { entry: ctx.read(ctx.refFor(id), false), created: true };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function hasRead(root, readerId) {
|
|
98
|
+
var records = ctx.store.all();
|
|
99
|
+
for (var i = 0; i < records.length; i++) {
|
|
100
|
+
if (records[i].rootId === root && records[i].op === "scheduled-result-read" && (records[i].readerId || null) === (readerId || null)) return true;
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function unread(viewerId, options) {
|
|
106
|
+
var entries = ctx.entries(false, viewerId);
|
|
107
|
+
var refs = [];
|
|
108
|
+
for (var i = 0; i < entries.length; i++) {
|
|
109
|
+
if (!entries[i].scheduledResult) continue;
|
|
110
|
+
if (ctx.matchesContext && !ctx.matchesContext(entries[i], options || {})) continue;
|
|
111
|
+
var located = ctx.findChain(entries[i].ref);
|
|
112
|
+
if (located && !hasRead(located.rootId, viewerId)) refs.push(entries[i].ref);
|
|
113
|
+
}
|
|
114
|
+
return { count: refs.length, newestRef: refs.length ? refs[0] : null, refs: refs };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function markRead(ref, viewerId, options) {
|
|
118
|
+
var entry = ctx.read(ref, false, viewerId);
|
|
119
|
+
if (!entry || !entry.scheduledResult) throw new Error("Scheduled result not found.");
|
|
120
|
+
var located = ctx.findChain(ref);
|
|
121
|
+
if (!located) throw new Error("Scheduled result not found.");
|
|
122
|
+
if (!hasRead(located.rootId, viewerId)) {
|
|
123
|
+
ctx.store.append({ id: recordStore.newRecordId(), rootId: located.rootId, op: "scheduled-result-read", scope: ctx.scopeId, readerId: viewerId || null, at: Date.now() });
|
|
124
|
+
}
|
|
125
|
+
return unread(viewerId, options);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { create: create, markRead: markRead, unread: unread };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { attachScheduledResultStore: attachScheduledResultStore, normalizeMetadata: normalizeMetadata, visibleTo: visibleTo };
|
|
@@ -50,8 +50,9 @@ function attachScheduledTaskExecution(ctx) {
|
|
|
50
50
|
var active = record.activeRun;
|
|
51
51
|
var driver = sm.sessions.get(active.driverSessionId);
|
|
52
52
|
var worker = sm.sessions.get(active.workerSessionId);
|
|
53
|
+
var finalSummary = clean(reason, 1000) || null;
|
|
53
54
|
var saved = registry.completeRun(recordId, runId, {
|
|
54
|
-
outcome: outcome,
|
|
55
|
+
outcome: outcome, summary: finalSummary, reason: finalSummary,
|
|
55
56
|
driverProviderSessionId: driver && driver.cliSessionId || active.driverProviderSessionId || null,
|
|
56
57
|
workerProviderSessionId: worker && worker.cliSessionId || active.workerProviderSessionId || null,
|
|
57
58
|
});
|
|
@@ -65,6 +66,37 @@ function attachScheduledTaskExecution(ctx) {
|
|
|
65
66
|
return true;
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
function recordTerminal(record, run, attempt) {
|
|
70
|
+
if (!record || !run || !run.runId || !run.outcome || typeof ctx.recordResult !== "function") return false;
|
|
71
|
+
try {
|
|
72
|
+
ctx.recordResult({
|
|
73
|
+
scheduleId: record.id, runId: run.runId, name: record.name || "Scheduled task",
|
|
74
|
+
ownerId: record.ownerId || null, outcome: run.outcome,
|
|
75
|
+
summary: run.summary || run.reason || null, startedAt: run.startedAt || null,
|
|
76
|
+
finishedAt: run.finishedAt || Date.now(), driverOriginId: run.driverOriginId || null,
|
|
77
|
+
workerOriginId: run.workerOriginId || null,
|
|
78
|
+
driverProviderSessionId: run.driverProviderSessionId || null,
|
|
79
|
+
workerProviderSessionId: run.workerProviderSessionId || null,
|
|
80
|
+
});
|
|
81
|
+
return true;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error("[scheduled-task] Failed to record result for " + run.runId + ":", error.message || error);
|
|
84
|
+
if (Number(attempt || 0) < 2) setTimeout(function () { recordTerminal(record, run, Number(attempt || 0) + 1); }, 1000);
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function syncTerminalResults() {
|
|
90
|
+
var records = registry.getAll ? registry.getAll() : [];
|
|
91
|
+
for (var i = 0; i < records.length; i++) {
|
|
92
|
+
if (records[i].createdViaScheduledTasks !== true && records[i].source !== "schedule") continue;
|
|
93
|
+
var runs = Array.isArray(records[i].runs) ? records[i].runs : [];
|
|
94
|
+
for (var j = 0; j < runs.length; j++) {
|
|
95
|
+
if (runs[j] && runs[j].runId && runs[j].outcome) recordTerminal(records[i], runs[j]);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
68
100
|
function failStart(recordId, runId, created, error, preDispatch) {
|
|
69
101
|
if (preDispatch) rollbackPair(created);
|
|
70
102
|
else if (created) {
|
|
@@ -110,6 +142,7 @@ function attachScheduledTaskExecution(ctx) {
|
|
|
110
142
|
name: record.name || "Scheduled task",
|
|
111
143
|
driver: record.execution.driver,
|
|
112
144
|
worker: record.execution.worker,
|
|
145
|
+
hidden: true,
|
|
113
146
|
});
|
|
114
147
|
var runState = {
|
|
115
148
|
status: "running", groupId: created.group.id,
|
|
@@ -230,7 +263,7 @@ function attachScheduledTaskExecution(ctx) {
|
|
|
230
263
|
return driver ? stopForSession(driver, reason) : finish(record.id, record.activeRun.runId, "interrupted", reason, false);
|
|
231
264
|
}
|
|
232
265
|
|
|
233
|
-
return { getToolDefs: getToolDefs, onPartnerResult: onPartnerResult, onTurnDone: onTurnDone, prepare: prepare, runnable: runnable, stopForSession: stopForSession, stopRecord: stopRecord, trigger: trigger };
|
|
266
|
+
return { getToolDefs: getToolDefs, onPartnerResult: onPartnerResult, onTurnDone: onTurnDone, prepare: prepare, recordTerminal: recordTerminal, runnable: runnable, stopForSession: stopForSession, stopRecord: stopRecord, syncTerminalResults: syncTerminalResults, trigger: trigger };
|
|
234
267
|
}
|
|
235
268
|
|
|
236
269
|
module.exports = { attachScheduledTaskExecution: attachScheduledTaskExecution, validRuntime: validRuntime };
|