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.
Files changed (119) hide show
  1. package/lib/context-usage.js +47 -0
  2. package/lib/issues-mcp-server.js +1 -1
  3. package/lib/issues-service.js +3 -1
  4. package/lib/loop-registry-occurrence.js +1 -0
  5. package/lib/notes.js +20 -3
  6. package/lib/os-users.js +6 -0
  7. package/lib/project-connection.js +14 -3
  8. package/lib/project-file-path.js +13 -0
  9. package/lib/project-file-watch.js +19 -8
  10. package/lib/project-filesystem.js +10 -16
  11. package/lib/project-http.js +53 -48
  12. package/lib/project-logs-mcp-server.js +8 -8
  13. package/lib/project-logs-schema.js +1 -1
  14. package/lib/project-logs-service.js +46 -13
  15. package/lib/project-logs-store.js +17 -15
  16. package/lib/project-logs.js +70 -0
  17. package/lib/project-loop.js +1 -0
  18. package/lib/project-notifications.js +1 -1
  19. package/lib/project-pending-message-queue.js +71 -7
  20. package/lib/project-scheduled-messages.js +235 -52
  21. package/lib/project-scheduled-result-store.js +131 -0
  22. package/lib/project-scheduled-task-execution.js +35 -2
  23. package/lib/project-session-notes.js +11 -4
  24. package/lib/project-sessions.js +7 -0
  25. package/lib/project-user-message.js +72 -5
  26. package/lib/project.js +60 -5
  27. package/lib/public/app.js +27 -12
  28. package/lib/public/css/avatar-imprints.css +1 -1
  29. package/lib/public/css/command-palette.css +20 -0
  30. package/lib/public/css/git-panel.css +13 -12
  31. package/lib/public/css/git-placard.css +4 -4
  32. package/lib/public/css/input.css +0 -109
  33. package/lib/public/css/issues.css +78 -31
  34. package/lib/public/css/mobile-nav.css +7 -0
  35. package/lib/public/css/notifications-center.css +3 -3
  36. package/lib/public/css/pending-message-queue.css +3 -0
  37. package/lib/public/css/project-logs.css +15 -0
  38. package/lib/public/css/sidebar.css +7 -0
  39. package/lib/public/css/sticky-notes.css +29 -0
  40. package/lib/public/modules/app-connection.js +9 -1
  41. package/lib/public/modules/app-favicon.js +10 -0
  42. package/lib/public/modules/app-messages.js +49 -37
  43. package/lib/public/modules/app-notifications.js +2 -0
  44. package/lib/public/modules/app-panels.js +77 -23
  45. package/lib/public/modules/app-projects.js +3 -0
  46. package/lib/public/modules/app-rate-limit.js +148 -243
  47. package/lib/public/modules/avatar-imprint.js +25 -70
  48. package/lib/public/modules/avatar.js +12 -4
  49. package/lib/public/modules/chat-render-runtime.js +20 -2
  50. package/lib/public/modules/clay-issue-links.js +23 -2
  51. package/lib/public/modules/clay-log-links.js +11 -1
  52. package/lib/public/modules/context-view-preference.js +143 -0
  53. package/lib/public/modules/default-vendor.js +116 -0
  54. package/lib/public/modules/filebrowser-tabs.js +1 -0
  55. package/lib/public/modules/filebrowser.js +45 -18
  56. package/lib/public/modules/input.js +81 -20
  57. package/lib/public/modules/issues-render.js +36 -10
  58. package/lib/public/modules/issues.js +95 -13
  59. package/lib/public/modules/markdown.js +3 -1
  60. package/lib/public/modules/pane-bridge.js +14 -8
  61. package/lib/public/modules/pane-issue-bridge.js +27 -0
  62. package/lib/public/modules/pending-message-queue.js +43 -1
  63. package/lib/public/modules/permission-hints.js +13 -0
  64. package/lib/public/modules/profile.js +9 -9
  65. package/lib/public/modules/project-activation.js +49 -0
  66. package/lib/public/modules/project-logs-render.js +13 -3
  67. package/lib/public/modules/project-logs.js +45 -15
  68. package/lib/public/modules/right-workbench.js +44 -0
  69. package/lib/public/modules/scheduled-message-state.js +12 -36
  70. package/lib/public/modules/scheduled-result-client.js +33 -0
  71. package/lib/public/modules/scheduled-result-display.js +50 -0
  72. package/lib/public/modules/scheduled-result-notification.js +31 -0
  73. package/lib/public/modules/scheduled-task-presentation.js +15 -0
  74. package/lib/public/modules/scheduled-tasks.js +9 -21
  75. package/lib/public/modules/sidebar-mobile.js +51 -3
  76. package/lib/public/modules/sidebar-sessions.js +65 -5
  77. package/lib/public/modules/split-session-boundary.js +28 -0
  78. package/lib/public/modules/split-view.js +42 -7
  79. package/lib/public/modules/sticky-notes-browser.js +130 -18
  80. package/lib/public/modules/sticky-notes.js +2 -3
  81. package/lib/public/modules/terminal.js +4 -4
  82. package/lib/public/modules/thinking-lifecycle.js +4 -1
  83. package/lib/public/modules/tools.js +5 -1
  84. package/lib/scheduled-run-state.js +10 -2
  85. package/lib/scheduler.js +1 -0
  86. package/lib/sdk-bridge.js +97 -61
  87. package/lib/sdk-message-processor.js +44 -13
  88. package/lib/sdk-skill-discovery.js +6 -28
  89. package/lib/sdk-turn-queue.js +14 -0
  90. package/lib/server-context-view.js +53 -0
  91. package/lib/server-default-vendor.js +60 -0
  92. package/lib/server.js +23 -6
  93. package/lib/session-notes-mcp-server.js +3 -5
  94. package/lib/session-pair-factory.js +2 -2
  95. package/lib/session-pair-mcp-server.js +5 -4
  96. package/lib/session-title-generator.js +89 -0
  97. package/lib/session-title-policy.js +8 -2
  98. package/lib/sessions.js +26 -1
  99. package/lib/user-presence.js +6 -0
  100. package/lib/users-context-view-preferences.js +65 -0
  101. package/lib/users-default-vendor-preferences.js +80 -0
  102. package/lib/users.js +12 -0
  103. package/lib/ws-schema.js +11 -0
  104. package/lib/yoke/acp-agent-profiles.js +15 -2
  105. package/lib/yoke/adapters/acp.js +3 -2
  106. package/lib/yoke/adapters/antigravity.js +2 -2
  107. package/lib/yoke/adapters/claude-worker.js +11 -12
  108. package/lib/yoke/adapters/claude.js +75 -22
  109. package/lib/yoke/adapters/codex.js +216 -29
  110. package/lib/yoke/adapters/kiro.js +6 -5
  111. package/lib/yoke/claude-context-usage.js +57 -0
  112. package/lib/yoke/claude-message-correlation.js +31 -0
  113. package/lib/yoke/claude-model-switch.js +15 -0
  114. package/lib/yoke/index.js +5 -1
  115. package/lib/yoke/mcp-bridge-server.js +18 -5
  116. package/lib/yoke/session-skill-catalog.js +45 -0
  117. package/lib/yoke/skill-discovery.js +60 -20
  118. package/lib/yoke/vendor-registry.js +1 -1
  119. 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: job.payload.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
- if (typeof text !== "string" || !text.trim() || !isFinite(Number(resetsAt))) {
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
- var actorId = users.isMultiUser() ? (actorUser && actorUser.id) : LOCAL_OWNER;
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
- session.scheduledMessage = projection(job);
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
- scheduler.cancel(job.id, reason || "cancelled_by_user");
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
- session.scheduledMessage = projection(updated);
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
- execution.recordReceipt({ state: "dispatching", projectSlug: slug, cliSessionId: session.cliSessionId, revision: job.payload.revision, recordedAt: now() });
241
-
242
- var userMessage = {
243
- type: "user_message",
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
- sm.sendAndRecord(session, { type: "scheduled_message_sent", jobId: job.id, revision: job.payload.revision });
265
- sm.broadcastSessionList();
266
- await Promise.resolve(queryPromise);
267
- if (session._scheduledMessageProcessing === processingOwner) delete session._scheduledMessageProcessing;
268
- return { dispatched: true, cliSessionId: session.cliSessionId, revision: job.payload.revision };
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) { session.scheduledMessage = projection(pendingJob(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, reason: clean(reason, 1000) || null,
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 };