clay-server 4.1.0-beta.5 → 4.1.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/LICENSE +661 -21
  2. package/LICENSES/MIT-legacy.txt +21 -0
  3. package/LICENSES/README.md +7 -0
  4. package/README.md +2 -2
  5. package/bin/cli.js +0 -2
  6. package/lib/autonomous-run-record.js +32 -0
  7. package/lib/autonomous-run-scheduler.js +92 -0
  8. package/lib/daemon.js +0 -2
  9. package/lib/default-ai-runtime.js +117 -0
  10. package/lib/durable-scheduler-record.js +61 -0
  11. package/lib/durable-scheduler-store.js +185 -0
  12. package/lib/durable-scheduler.js +495 -0
  13. package/lib/git-cli.js +1 -0
  14. package/lib/issue-work-session.js +10 -0
  15. package/lib/issues-comments.js +40 -0
  16. package/lib/issues-commit-verification.js +30 -0
  17. package/lib/issues-mcp-server.js +27 -0
  18. package/lib/issues-schema.js +119 -0
  19. package/lib/issues-service.js +339 -0
  20. package/lib/issues-store.js +210 -0
  21. package/lib/loop-cron.js +47 -0
  22. package/lib/loop-durable-schedule.js +159 -0
  23. package/lib/loop-guidance.js +35 -0
  24. package/lib/loop-registry-occurrence.js +76 -0
  25. package/lib/model-entry.js +15 -0
  26. package/lib/project-autonomous-run.js +498 -0
  27. package/lib/project-capsule-turn.js +1 -0
  28. package/lib/project-connection.js +6 -1
  29. package/lib/project-delegated-session.js +1 -0
  30. package/lib/project-filesystem.js +10 -10
  31. package/lib/project-full-access.js +86 -0
  32. package/lib/project-http.js +3 -1
  33. package/lib/project-issue-launch.js +154 -0
  34. package/lib/project-issues.js +58 -0
  35. package/lib/project-logs-mcp-server.js +8 -12
  36. package/lib/project-loop-interview.js +163 -0
  37. package/lib/project-loop.js +150 -64
  38. package/lib/project-message-delivery.js +4 -2
  39. package/lib/project-models.js +71 -3
  40. package/lib/project-pair-autonomous-stop.js +16 -0
  41. package/lib/project-pair-owned-stop.js +18 -0
  42. package/lib/project-pair-task-control.js +14 -1
  43. package/lib/project-pending-message-queue.js +264 -0
  44. package/lib/project-scheduled-messages.js +316 -0
  45. package/lib/project-scheduled-task-execution.js +236 -0
  46. package/lib/project-scheduled-tasks.js +481 -0
  47. package/lib/project-session-pair.js +16 -19
  48. package/lib/project-sessions.js +114 -31
  49. package/lib/project-tool-llm-runtime.js +48 -0
  50. package/lib/project-user-message.js +202 -60
  51. package/lib/project-workspace-query.js +25 -0
  52. package/lib/project.js +361 -69
  53. package/lib/public/app.js +85 -22
  54. package/lib/public/css/default-ai.css +33 -0
  55. package/lib/public/css/home-hub.css +15 -2
  56. package/lib/public/css/home-sidebar.css +50 -2
  57. package/lib/public/css/input.css +160 -5
  58. package/lib/public/css/issues.css +101 -0
  59. package/lib/public/css/messages.css +19 -0
  60. package/lib/public/css/pane.css +58 -7
  61. package/lib/public/css/pending-message-queue.css +187 -0
  62. package/lib/public/css/rewind.css +11 -2
  63. package/lib/public/css/scheduled-tasks.css +122 -0
  64. package/lib/public/index.html +42 -150
  65. package/lib/public/modules/app-connection.js +3 -0
  66. package/lib/public/modules/app-cursors.js +29 -12
  67. package/lib/public/modules/app-loop-ui.js +4 -8
  68. package/lib/public/modules/app-message-router.js +8 -0
  69. package/lib/public/modules/app-messages.js +34 -9
  70. package/lib/public/modules/app-projects.js +2 -0
  71. package/lib/public/modules/app-rate-limit.js +8 -5
  72. package/lib/public/modules/autonomous-run.js +285 -0
  73. package/lib/public/modules/clay-file-links.js +111 -0
  74. package/lib/public/modules/clay-issue-links.js +131 -0
  75. package/lib/public/modules/clay-log-links.js +2 -0
  76. package/lib/public/modules/cursor-sharing-preference.js +38 -0
  77. package/lib/public/modules/default-ai.js +377 -0
  78. package/lib/public/modules/filebrowser.js +43 -2
  79. package/lib/public/modules/home-sidebar.js +16 -8
  80. package/lib/public/modules/home-surface.js +5 -0
  81. package/lib/public/modules/input.js +11 -20
  82. package/lib/public/modules/issues-context.js +117 -0
  83. package/lib/public/modules/issues-render.js +29 -0
  84. package/lib/public/modules/issues.js +270 -0
  85. package/lib/public/modules/loop-interview.js +151 -0
  86. package/lib/public/modules/markdown.js +4 -0
  87. package/lib/public/modules/message-delivery.js +5 -3
  88. package/lib/public/modules/pending-message-image-editor.js +143 -0
  89. package/lib/public/modules/pending-message-queue-model.js +22 -0
  90. package/lib/public/modules/pending-message-queue.js +449 -0
  91. package/lib/public/modules/project-logs.js +2 -2
  92. package/lib/public/modules/scheduled-message-state.js +46 -0
  93. package/lib/public/modules/scheduled-task-edit-state.js +7 -0
  94. package/lib/public/modules/scheduled-task-engine.js +48 -0
  95. package/lib/public/modules/scheduled-task-form.js +122 -0
  96. package/lib/public/modules/scheduled-task-recurrence.js +46 -0
  97. package/lib/public/modules/scheduled-task-schedule-rule.js +115 -0
  98. package/lib/public/modules/scheduled-tasks.js +459 -0
  99. package/lib/public/modules/scheduler-config.js +6 -52
  100. package/lib/public/modules/scheduler.js +9 -21
  101. package/lib/public/modules/sidebar-mobile.js +3 -0
  102. package/lib/public/modules/sidebar.js +0 -11
  103. package/lib/public/modules/split-view.js +2 -0
  104. package/lib/public/modules/sticky-note-markdown.js +1 -0
  105. package/lib/public/modules/terminal.js +2 -0
  106. package/lib/public/modules/tool-palette-order.js +3 -3
  107. package/lib/public/modules/tools.js +5 -2
  108. package/lib/public/style.css +3 -0
  109. package/lib/record-revision.js +11 -0
  110. package/lib/schedule-rule.js +151 -0
  111. package/lib/schedule-validation.js +124 -0
  112. package/lib/scheduled-run-state.js +87 -0
  113. package/lib/scheduled-task-records.js +127 -0
  114. package/lib/scheduler.js +179 -196
  115. package/lib/sdk-bridge.js +157 -22
  116. package/lib/sdk-message-processor.js +3 -0
  117. package/lib/server-cursor-sharing.js +65 -0
  118. package/lib/server-default-ai.js +265 -0
  119. package/lib/server-durable-scheduler.js +26 -0
  120. package/lib/server-home-chat.js +29 -9
  121. package/lib/server-home-clay-entry.js +4 -1
  122. package/lib/server-home-debate-planning.js +32 -14
  123. package/lib/server-home-mate-creation.js +30 -11
  124. package/lib/server-home-models.js +14 -6
  125. package/lib/server-issue-links.js +18 -0
  126. package/lib/server.js +63 -0
  127. package/lib/session-notes-mcp-server.js +1 -1
  128. package/lib/session-pair-factory.js +42 -4
  129. package/lib/session-pair-query-start.js +22 -0
  130. package/lib/session-query-tool-bridge.js +52 -0
  131. package/lib/session-search.js +4 -1
  132. package/lib/session-split-groups.js +35 -1
  133. package/lib/sessions.js +51 -3
  134. package/lib/tool-llm.js +1 -0
  135. package/lib/users-default-ai-preferences.js +80 -0
  136. package/lib/users-home-surface-preferences.js +2 -1
  137. package/lib/users-preferences.js +35 -0
  138. package/lib/users-scheduled-task-preferences.js +44 -0
  139. package/lib/users.js +16 -0
  140. package/lib/workspace-query-projection.js +29 -0
  141. package/lib/workspace-query-service.js +19 -29
  142. package/lib/ws-schema.js +61 -1
  143. package/lib/yoke/adapters/antigravity.js +1 -0
  144. package/lib/yoke/adapters/claude-worker.js +11 -6
  145. package/lib/yoke/adapters/claude.js +35 -1
  146. package/lib/yoke/adapters/codex.js +3 -0
  147. package/lib/yoke/claude-user-input.js +2 -0
  148. package/lib/yoke/index.js +2 -0
  149. package/lib/yoke/mcp-bridge-server.js +7 -1
  150. package/lib/yoke/package.json +1 -1
  151. package/lib/yoke/session-tool-transport.js +53 -0
  152. package/lib/yoke/user-input.js +21 -7
  153. package/package.json +5 -6
  154. package/bin/claude-relay.js +0 -6
  155. package/lib/public/modules/app-loop-wizard.js +0 -439
@@ -0,0 +1,264 @@
1
+ var fs = require("fs");
2
+ var path = require("path");
3
+ var crypto = require("crypto");
4
+ var config = require("./config");
5
+ var sessionProvenance = require("./session-provenance");
6
+
7
+ var MAX_TEXT = 100000;
8
+ var MAX_PASTES = 20;
9
+ var MAX_PASTE_LENGTH = 500000;
10
+ var MAX_IMAGES = 20;
11
+ var MAX_IMAGE_DATA = 25 * 1024 * 1024;
12
+ var MAX_TOTAL_BYTES = 32 * 1024 * 1024;
13
+ var MAX_ACTIVE_ITEMS = 100;
14
+ var IMAGE_TYPE_RE = /^image\/[a-z0-9.+-]{1,64}$/i;
15
+ var CLIENT_MESSAGE_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
16
+
17
+ function defaultFilePath(opts) {
18
+ var identity = String(opts.slug || "") + "\n" + path.resolve(opts.cwd || process.cwd());
19
+ return path.join(config.CONFIG_DIR, "pending-message-queues", crypto.createHash("sha256").update(identity).digest("hex") + ".json");
20
+ }
21
+
22
+ function createPendingMessageQueue(opts) {
23
+ var filePath = opts.filePath || defaultFilePath(opts);
24
+ var broadcast = opts.broadcast || opts.send || function () {};
25
+ var authorize = opts.authorize || function () { return true; };
26
+ var canMutate = opts.canMutate || authorize;
27
+ var loaded = load();
28
+ var data = loaded.data;
29
+ var loadError = loaded.error;
30
+
31
+ function emptyData() { return { version: 1, revision: 0, nextId: 1, items: [], paused: {} }; }
32
+ function validStored(parsed) {
33
+ if (!parsed || parsed.version !== 1 || !Number.isInteger(parsed.revision) || parsed.revision < 0 || !Number.isInteger(parsed.nextId) || parsed.nextId < 1 || !Array.isArray(parsed.items)) return false;
34
+ if (parsed.paused !== undefined && (!parsed.paused || typeof parsed.paused !== "object" || Array.isArray(parsed.paused))) return false;
35
+ var pausedKeys = Object.keys(parsed.paused || {});
36
+ for (var pausedIndex = 0; pausedIndex < pausedKeys.length; pausedIndex++) if (pausedKeys[pausedIndex].length > 300 || parsed.paused[pausedKeys[pausedIndex]] !== true) return false;
37
+ for (var i = 0; i < parsed.items.length; i++) {
38
+ var item = parsed.items[i];
39
+ if (!item || typeof item.id !== "string" || typeof item.sessionKey !== "string" || typeof item.projectSlug !== "string" || typeof item.actorId !== "string" || !item.message || ["pending", "claimed", "consumed", "cancelled", "failed"].indexOf(item.state) < 0) return false;
40
+ }
41
+ return true;
42
+ }
43
+ function load() {
44
+ if (!fs.existsSync(filePath)) return { data: emptyData(), error: null };
45
+ try {
46
+ var parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
47
+ if (!validStored(parsed)) return { data: emptyData(), error: "Queue storage is malformed" };
48
+ if (!parsed.paused) parsed.paused = {};
49
+ return { data: parsed, error: null };
50
+ } catch (e) {
51
+ return { data: emptyData(), error: "Queue storage is unreadable" };
52
+ }
53
+ }
54
+ function save() {
55
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
56
+ var tmp = filePath + ".tmp." + process.pid + "." + crypto.randomBytes(6).toString("hex");
57
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
58
+ fs.renameSync(tmp, filePath);
59
+ }
60
+ var persist = opts.persist || save;
61
+ function key(session) {
62
+ var hadOrigin = !!(session && session.sessionOriginId);
63
+ var origin = sessionProvenance.ensureOrigin(session);
64
+ if (!hadOrigin && origin && typeof opts.ensureSessionIdentity === "function") opts.ensureSessionIdentity(session);
65
+ return String(origin);
66
+ }
67
+ function actorId(actor) { return actor && actor.id ? String(actor.id) : "default"; }
68
+ function copy(value) { return JSON.parse(JSON.stringify(value)); }
69
+ function restore(snapshot) { data.version = snapshot.version; data.revision = snapshot.revision; data.nextId = snapshot.nextId; data.items = snapshot.items; data.paused = snapshot.paused || {}; }
70
+ function targetKey(session) { return String(opts.slug) + ":" + key(session); }
71
+ function isPaused(session) { return !!(session && data.paused[targetKey(session)]); }
72
+ function sameTarget(item, session) { return !!(item && session && item.sessionKey === key(session) && item.projectSlug === opts.slug); }
73
+ function scoped(item, session, actor) { return sameTarget(item, session) && item.actorId === actorId(actor); }
74
+ function visible(session, actor) {
75
+ if (loadError || !session || !authorize(session, actor)) return [];
76
+ var out = [];
77
+ for (var i = 0; i < data.items.length; i++) if (sameTarget(data.items[i], session)) out.push(copy(data.items[i]));
78
+ return out;
79
+ }
80
+ function result(ok, error, session, actor) {
81
+ return { ok: ok, error: error || null, revision: data.revision, sessionId: session ? session.localId : null, projectSlug: opts.slug, paused: session ? isPaused(session) : false, items: session ? visible(session, actor) : [] };
82
+ }
83
+ function validateMessage(message) {
84
+ if (!message || typeof message !== "object" || Array.isArray(message)) return "Invalid message";
85
+ var text = message.text == null ? "" : message.text;
86
+ if (typeof text !== "string" || text.length > MAX_TEXT) return "Message text is invalid or too long";
87
+ var pastes = message.pastes == null ? [] : message.pastes;
88
+ if (!Array.isArray(pastes) || pastes.length > MAX_PASTES) return "Too many paste attachments";
89
+ for (var i = 0; i < pastes.length; i++) if (typeof pastes[i] !== "string" || pastes[i].length > MAX_PASTE_LENGTH) return "Invalid paste attachment";
90
+ var images = message.images == null ? [] : message.images;
91
+ if (!Array.isArray(images) || images.length > MAX_IMAGES) return "Too many image attachments";
92
+ for (var j = 0; j < images.length; j++) {
93
+ var image = images[j];
94
+ if (!image || typeof image !== "object" || !IMAGE_TYPE_RE.test(image.mediaType || "") || typeof image.data !== "string" || image.data.length > MAX_IMAGE_DATA) return "Invalid image attachment";
95
+ }
96
+ if (!text.trim() && !pastes.length && !images.length) return "Message must contain text or an attachment";
97
+ if (message.clientMessageId != null && (typeof message.clientMessageId !== "string" || !CLIENT_MESSAGE_ID_RE.test(message.clientMessageId))) return "Invalid client message identifier";
98
+ try { if (Buffer.byteLength(JSON.stringify(message), "utf8") > MAX_TOTAL_BYTES) return "Message payload is too large"; }
99
+ catch (e) { return "Invalid message payload"; }
100
+ return null;
101
+ }
102
+ function sanitizeImages(images) {
103
+ if (!Array.isArray(images)) return images;
104
+ return images.map(function (image) { return { mediaType: image.mediaType, data: image.data }; });
105
+ }
106
+ function inspect(session, actor, clientMessageId) {
107
+ if (loadError || !clientMessageId) return null;
108
+ for (var i = 0; i < data.items.length; i++) if (data.items[i].clientMessageId === clientMessageId && scoped(data.items[i], session, actor)) return copy(data.items[i]);
109
+ return null;
110
+ }
111
+ function hasActive(session) {
112
+ if (loadError || !session) return false;
113
+ for (var i = 0; i < data.items.length; i++) if (sameTarget(data.items[i], session) && (data.items[i].state === "pending" || data.items[i].state === "claimed")) return true;
114
+ return false;
115
+ }
116
+ function admit(session, message, actor) {
117
+ if (loadError) return result(false, loadError, session, actor);
118
+ if (!session || !canMutate(session, actor, null)) return result(false, "Not authorized", session, actor);
119
+ var validationError = validateMessage(message);
120
+ if (validationError) return result(false, validationError, session, actor);
121
+ var activeCount = 0;
122
+ for (var activeIndex = 0; activeIndex < data.items.length; activeIndex++) {
123
+ if (sameTarget(data.items[activeIndex], session) && (data.items[activeIndex].state === "pending" || data.items[activeIndex].state === "claimed")) activeCount++;
124
+ }
125
+ if (activeCount >= MAX_ACTIVE_ITEMS) return result(false, "Pending message queue is full", session, actor);
126
+ var clientId = message.clientMessageId ? String(message.clientMessageId) : null;
127
+ var duplicate = inspect(session, actor, clientId);
128
+ if (duplicate) return result(duplicate.state !== "cancelled" && duplicate.state !== "failed", duplicate.state === "cancelled" ? "Message was cancelled" : duplicate.state === "failed" ? "Message delivery failed" : null, session, actor);
129
+ var before = copy(data);
130
+ var storedMessage = copy(message);
131
+ if (Object.prototype.hasOwnProperty.call(storedMessage, "images")) storedMessage.images = sanitizeImages(storedMessage.images);
132
+ var item = { id: "pm_" + data.nextId++, sessionKey: key(session), sessionLocalId: session.localId, projectSlug: opts.slug, actorId: actorId(actor), actorName: actor && (actor.displayName || actor.username || actor.name) ? String(actor.displayName || actor.username || actor.name).slice(0, 120) : null, clientMessageId: clientId, message: storedMessage, createdAt: Date.now(), state: "pending" };
133
+ delete item.message._pendingConsumed;
134
+ data.items.push(item);
135
+ data.revision++;
136
+ try { persist(); } catch (e) { restore(before); return result(false, "Queue storage failed", session, actor); }
137
+ broadcast(session, { type: "pending_message_queued", sessionId: session.localId, projectSlug: opts.slug, paused: isPaused(session), item: copy(item), revision: data.revision });
138
+ return result(true, null, session, actor);
139
+ }
140
+ function mutate(session, actor, id, expectedRevision, expectedSessionId, operation, allowedStates) {
141
+ if (loadError) return result(false, loadError, session, actor);
142
+ if (!session || session.localId !== expectedSessionId || !authorize(session, actor)) return result(false, "Session changed", session, actor);
143
+ if (expectedRevision !== data.revision) return result(false, "Stale queue revision", session, actor);
144
+ var index = -1;
145
+ for (var i = 0; i < data.items.length; i++) if (data.items[i].id === id && sameTarget(data.items[i], session)) index = i;
146
+ var mutableStates = allowedStates || ["pending"];
147
+ if (index < 0 || mutableStates.indexOf(data.items[index].state) < 0 || !canMutate(session, actor, data.items[index])) return result(false, "Pending message not found", session, actor);
148
+ var before = copy(data);
149
+ operation(data.items, index);
150
+ data.revision++;
151
+ try { persist(); } catch (e) { restore(before); return result(false, "Queue storage failed", session, actor); }
152
+ if (data.items[index].state === "cancelled" && session._pendingMessageClaimId === data.items[index].id) delete session._pendingMessageClaimId;
153
+ broadcast(session, { type: "pending_message_state", sessionId: session.localId, projectSlug: opts.slug, paused: isPaused(session), revision: data.revision, items: visible(session, actor) });
154
+ return result(true, null, session, actor);
155
+ }
156
+ function edit(session, actor, id, revision, expectedSessionId, message) {
157
+ var current = null;
158
+ for (var i = 0; i < data.items.length; i++) if (data.items[i].id === id && sameTarget(data.items[i], session)) current = data.items[i];
159
+ var text = message && message.text;
160
+ if (typeof text !== "string" || text.length > MAX_TEXT) return result(false, "Edited text is invalid or too long", session, actor);
161
+ var proposed = current ? Object.assign({}, current.message, { text: text }) : null;
162
+ if (message && Object.prototype.hasOwnProperty.call(message, "images")) {
163
+ proposed = Object.assign({}, proposed, { images: message.images });
164
+ }
165
+ if (current) {
166
+ var validationError = validateMessage(proposed);
167
+ if (validationError) return result(false, validationError, session, actor);
168
+ }
169
+ return mutate(session, actor, id, revision, expectedSessionId, function (items, index) {
170
+ var nextMessage = Object.assign({}, items[index].message, { text: text });
171
+ if (message && Object.prototype.hasOwnProperty.call(message, "images")) nextMessage.images = sanitizeImages(message.images);
172
+ items[index].message = nextMessage;
173
+ });
174
+ }
175
+ function cancel(session, actor, id, revision, expectedSessionId) {
176
+ return mutate(session, actor, id, revision, expectedSessionId, function (items, index) { items[index].state = "cancelled"; }, ["pending", "claimed"]);
177
+ }
178
+ function reorder(session, actor, ids, revision, expectedSessionId) {
179
+ if (loadError) return result(false, loadError, session, actor);
180
+ var positions = [];
181
+ var pending = [];
182
+ for (var i = 0; i < data.items.length; i++) if (scoped(data.items[i], session, actor) && data.items[i].state === "pending") { positions.push(i); pending.push(data.items[i].id); }
183
+ if (!Array.isArray(ids) || ids.length !== pending.length) return result(false, "Reorder must include every owned pending message", session, actor);
184
+ var seen = {};
185
+ for (var j = 0; j < ids.length; j++) {
186
+ if (seen[ids[j]] || pending.indexOf(ids[j]) < 0) return result(false, "Reorder contains a duplicate, foreign, or nonpending message", session, actor);
187
+ seen[ids[j]] = true;
188
+ }
189
+ if (!ids.length) return result(true, null, session, actor);
190
+ return mutate(session, actor, ids[0], revision, expectedSessionId, function (items) {
191
+ var byId = {};
192
+ for (var k = 0; k < positions.length; k++) byId[items[positions[k]].id] = items[positions[k]];
193
+ for (var n = 0; n < positions.length; n++) items[positions[n]] = byId[ids[n]];
194
+ });
195
+ }
196
+ function pause(session, actor) {
197
+ if (loadError || !session || !authorize(session, actor)) return false;
198
+ if (!hasActive(session)) return true;
199
+ if (isPaused(session)) return true;
200
+ var before = copy(data);
201
+ data.paused[targetKey(session)] = true;
202
+ data.revision++;
203
+ try { persist(); } catch (e) { restore(before); return false; }
204
+ broadcast(session, { type: "pending_message_state", sessionId: session.localId, projectSlug: opts.slug, paused: true, revision: data.revision, items: visible(session, actor) });
205
+ return true;
206
+ }
207
+ function resume(session, actor, expectedRevision, expectedSessionId) {
208
+ if (loadError) return result(false, loadError, session, actor);
209
+ if (!session || session.localId !== expectedSessionId || !authorize(session, actor)) return result(false, "Session changed", session, actor);
210
+ if (expectedRevision !== data.revision) return result(false, "Stale queue revision", session, actor);
211
+ if (!isPaused(session)) return result(true, null, session, actor);
212
+ var before = copy(data);
213
+ delete data.paused[targetKey(session)];
214
+ data.revision++;
215
+ try { persist(); } catch (e) { restore(before); return result(false, "Queue storage failed", session, actor); }
216
+ broadcast(session, { type: "pending_message_state", sessionId: session.localId, projectSlug: opts.slug, paused: false, revision: data.revision, items: visible(session, actor) });
217
+ return result(true, null, session, actor);
218
+ }
219
+ function finishClaim(session, id, ok, error) {
220
+ var index = -1;
221
+ for (var i = 0; i < data.items.length; i++) if (data.items[i].id === id && sameTarget(data.items[i], session) && data.items[i].state === "claimed") index = i;
222
+ if (index < 0) return false;
223
+ var before = copy(data);
224
+ data.items[index].state = ok === "release" ? "pending" : ok ? "consumed" : "failed";
225
+ if (ok === "release") delete data.items[index].claimedAt;
226
+ else data.items[index][ok ? "consumedAt" : "failedAt"] = Date.now();
227
+ if (!ok) data.items[index].failure = error || "Pending message dispatch was rejected";
228
+ data.revision++;
229
+ try { persist(); } catch (e) { restore(before); return false; }
230
+ if (session._pendingMessageClaimId === id) delete session._pendingMessageClaimId;
231
+ broadcast(session, { type: ok === "release" ? "pending_message_state" : "pending_message_consumed", sessionId: session.localId, projectSlug: opts.slug, paused: isPaused(session), id: id, state: data.items[index].state, items: ok === "release" ? visible(session, { id: data.items[index].actorId }) : undefined, revision: data.revision });
232
+ return true;
233
+ }
234
+ function consumeOne(session, dispatch) {
235
+ if (loadError || !session || session._pendingMessageClaimId || isPaused(session) || session._pendingMessageDrainPaused || session.taskStopRequested || session._queryStarting || session._awaitingTurnResult || session.isProcessing) return null;
236
+ if (session.rateLimitResetsAt && session.rateLimitResetsAt > Date.now()) return null;
237
+ for (var i = 0; i < data.items.length; i++) {
238
+ var item = data.items[i];
239
+ if (!sameTarget(item, session) || item.state !== "pending") continue;
240
+ if (!canMutate(session, { id: item.actorId }, item)) continue;
241
+ var before = copy(data);
242
+ item.state = "claimed";
243
+ item.claimedAt = Date.now();
244
+ data.revision++;
245
+ try { persist(); } catch (e) { restore(before); return null; }
246
+ session._pendingMessageClaimId = item.id;
247
+ broadcast(session, { type: "pending_message_claimed", sessionId: session.localId, projectSlug: opts.slug, paused: isPaused(session), id: item.id, revision: data.revision });
248
+ var completed = false;
249
+ function complete(ok, error) { if (completed) return false; completed = true; return finishClaim(session, item.id, ok, error); }
250
+ try { if (dispatch(copy(item), complete) === false) complete(false, "Pending message dispatch was rejected"); }
251
+ catch (e) { complete(false, e.message || String(e)); }
252
+ return copy(item);
253
+ }
254
+ return null;
255
+ }
256
+ function hydrate(session, actor) { return result(!loadError, loadError, session, actor); }
257
+ function isClaimed(session, id) {
258
+ for (var i = 0; i < data.items.length; i++) if (data.items[i].id === id && sameTarget(data.items[i], session) && data.items[i].state === "claimed") return true;
259
+ return false;
260
+ }
261
+ return { admit: admit, edit: edit, cancel: cancel, reorder: reorder, pause: pause, resume: resume, consumeOne: consumeOne, hydrate: hydrate, inspect: inspect, hasActive: hasActive, isClaimed: isClaimed, isPaused: isPaused, list: visible, getRevision: function () { return data.revision; }, getLoadError: function () { return loadError; }, filePath: filePath };
262
+ }
263
+
264
+ module.exports = { createPendingMessageQueue: createPendingMessageQueue };
@@ -0,0 +1,316 @@
1
+ var crypto = require("crypto");
2
+ var sessionProvenance = require("./session-provenance");
3
+
4
+ var LOCAL_OWNER = "local-user";
5
+
6
+ function attachProjectScheduledMessages(ctx) {
7
+ var scheduler = ctx.scheduler;
8
+ var slug = ctx.slug;
9
+ var sm = ctx.sm;
10
+ var sdk = ctx.sdk;
11
+ var users = ctx.usersModule;
12
+ var type = "project-chat-message:" + slug;
13
+ var makeRevision = ctx.makeRevision || function () { return crypto.randomUUID(); };
14
+ var now = ctx.now || Date.now;
15
+ var unregister = null;
16
+ var stopped = false;
17
+
18
+ function findSession(cliSessionId) {
19
+ var found = null;
20
+ sm.sessions.forEach(function (session) {
21
+ if (!found && session.cliSessionId === cliSessionId) found = session;
22
+ });
23
+ return found;
24
+ }
25
+
26
+ function latestJob(session) {
27
+ if (!scheduler || !session || !session.cliSessionId) return null;
28
+ var jobs = scheduler.listJobs();
29
+ for (var i = jobs.length - 1; i >= 0; i--) {
30
+ if (jobs[i].type === type && jobs[i].projectId === slug && jobs[i].target && jobs[i].target.id === session.cliSessionId) return jobs[i];
31
+ }
32
+ return null;
33
+ }
34
+
35
+ function pendingJob(session) {
36
+ if (!scheduler || !session || !session.cliSessionId) return null;
37
+ if (session.scheduledMessage && session.scheduledMessage.jobId) {
38
+ var direct = scheduler.getJob(session.scheduledMessage.jobId);
39
+ if (direct && direct.state === "queued") return direct;
40
+ }
41
+ var jobs = scheduler.listJobs();
42
+ for (var i = jobs.length - 1; i >= 0; i--) {
43
+ if (jobs[i].type === type && jobs[i].projectId === slug && jobs[i].state === "queued" &&
44
+ jobs[i].target && jobs[i].target.id === session.cliSessionId) return jobs[i];
45
+ }
46
+ return null;
47
+ }
48
+
49
+ function projection(job) {
50
+ if (!job || job.state !== "queued") return null;
51
+ return {
52
+ jobId: job.id,
53
+ revision: job.payload.revision,
54
+ text: job.payload.text,
55
+ resetsAt: job.runAt,
56
+ scheduledAt: job.payload.scheduledAt,
57
+ };
58
+ }
59
+
60
+ function sendState(session, targetWs, sendFn) {
61
+ var job = pendingJob(session);
62
+ session.scheduledMessage = projection(job);
63
+ var latest = latestJob(session);
64
+ var interrupted = !job && latest && latest.state === "interrupted" ? {
65
+ jobId: latest.id,
66
+ revision: latest.payload.revision,
67
+ text: latest.payload.text,
68
+ } : null;
69
+ var message = { type: "scheduled_message_state", scheduledMessage: session.scheduledMessage, interrupted: interrupted };
70
+ if (sendFn) sendFn(message);
71
+ else if (targetWs) ctx.sendTo(targetWs, message);
72
+ else ctx.sendToSession(session.localId, message);
73
+ return message;
74
+ }
75
+
76
+ function visibleError(session, targetWs, text) {
77
+ var message = { type: "error", text: text };
78
+ if (targetWs) ctx.sendTo(targetWs, message);
79
+ else if (session) ctx.sendToSession(session.localId, message);
80
+ }
81
+
82
+ function ownerFor(session) {
83
+ if (!users.isMultiUser()) return LOCAL_OWNER;
84
+ if (!session.ownerId) throw new Error("Scheduled messages require a session owner.");
85
+ if (!users.findUserById(session.ownerId)) throw new Error("The session owner no longer exists.");
86
+ return session.ownerId;
87
+ }
88
+
89
+ function authorize(session, actorId) {
90
+ if (!session || session.destroying) throw new Error("The session is no longer available.");
91
+ if (sm.sessions.get(session.localId) !== session) throw new Error("The session is no longer live.");
92
+ if (!session.cliSessionId) throw new Error("This session has no persistent provider identity yet.");
93
+ if (session.mode === "tui" || session.runtimeMode === "tui" || sessionProvenance.isWorker(session) ||
94
+ (ctx.isDriverOperatedSession && ctx.isDriverOperatedSession(session))) {
95
+ throw new Error("Scheduled messages are available only for ordinary project chat sessions.");
96
+ }
97
+ if (!users.isMultiUser()) return LOCAL_OWNER;
98
+ if (!actorId || !users.findUserById(actorId)) throw new Error("The scheduling user no longer exists.");
99
+ var access = ctx.getProjectAccess();
100
+ if (!ctx.canAccessProjectSlug(actorId, slug) || !users.canAccessSession(actorId, session, access)) {
101
+ throw new Error("Scheduled message access was revoked.");
102
+ }
103
+ var ownerId = ownerFor(session);
104
+ if (!ctx.canAccessProjectSlug(ownerId, slug) || !users.canAccessSession(ownerId, session, access)) {
105
+ throw new Error("The session owner no longer has access.");
106
+ }
107
+ return ownerId;
108
+ }
109
+
110
+ function buildSpec(session, text, resetsAt, actorId, revision) {
111
+ var timestamp = now();
112
+ var runAt = resetsAt <= timestamp ? timestamp + 5000 : resetsAt + 60000;
113
+ var ownerId = authorize(session, actorId);
114
+ return {
115
+ type: type,
116
+ idempotencyKey: "project-message:" + slug + ":" + session.cliSessionId + ":" + revision,
117
+ ownerId: ownerId,
118
+ projectId: slug,
119
+ target: { kind: "project-session", id: session.cliSessionId },
120
+ payload: {
121
+ text: text,
122
+ cliSessionId: session.cliSessionId,
123
+ actorId: actorId || LOCAL_OWNER,
124
+ sessionOwnerId: ownerId,
125
+ revision: revision,
126
+ requestedResetsAt: resetsAt,
127
+ scheduledAt: timestamp,
128
+ },
129
+ runAt: runAt,
130
+ };
131
+ }
132
+
133
+ function schedule(session, text, resetsAt, actorUser, targetWs) {
134
+ if (!scheduler || stopped) {
135
+ visibleError(session, targetWs, "Scheduled messages are temporarily unavailable.");
136
+ return null;
137
+ }
138
+ if (typeof text !== "string" || !text.trim() || !isFinite(Number(resetsAt))) {
139
+ visibleError(session, targetWs, "A valid scheduled message and reset time are required.");
140
+ return null;
141
+ }
142
+ try {
143
+ var actorId = users.isMultiUser() ? (actorUser && actorUser.id) : LOCAL_OWNER;
144
+ var revision = makeRevision();
145
+ var spec = buildSpec(session, text, Number(resetsAt), actorId, revision);
146
+ var current = pendingJob(session);
147
+ var job = current ? scheduler.replaceQueued(current.id, spec) : scheduler.enqueue(spec);
148
+ session.scheduledMessage = projection(job);
149
+ sm.sendAndRecord(session, Object.assign({ type: "scheduled_message_queued" }, session.scheduledMessage));
150
+ return job;
151
+ } catch (error) {
152
+ if (session) session.rateLimitAutoContinuePending = false;
153
+ visibleError(session, targetWs, "Could not schedule the message: " + error.message);
154
+ return null;
155
+ }
156
+ }
157
+
158
+ function requireCorrelation(job, correlation, targetWs) {
159
+ if (!targetWs) return;
160
+ if (!correlation || correlation.jobId !== job.id || correlation.revision !== job.payload.revision) {
161
+ throw new Error("The scheduled-message control is stale.");
162
+ }
163
+ }
164
+
165
+ function cancel(session, actorUser, targetWs, reason, correlation) {
166
+ try {
167
+ authorize(session, users.isMultiUser() ? (actorUser && actorUser.id) : LOCAL_OWNER);
168
+ var job = pendingJob(session);
169
+ if (!job) return false;
170
+ requireCorrelation(job, correlation, targetWs);
171
+ scheduler.cancel(job.id, reason || "cancelled_by_user");
172
+ session.scheduledMessage = null;
173
+ session.rateLimitAutoContinuePending = false;
174
+ sm.sendAndRecord(session, { type: "scheduled_message_cancelled", jobId: job.id, revision: job.payload.revision });
175
+ return true;
176
+ } catch (error) {
177
+ visibleError(session, targetWs, "Could not cancel the scheduled message: " + error.message);
178
+ return false;
179
+ }
180
+ }
181
+
182
+ function sendNow(session, actorUser, targetWs, correlation) {
183
+ try {
184
+ authorize(session, users.isMultiUser() ? (actorUser && actorUser.id) : LOCAL_OWNER);
185
+ var job = pendingJob(session);
186
+ if (!job) return false;
187
+ requireCorrelation(job, correlation, targetWs);
188
+ var updated = scheduler.replaceQueued(job.id, {
189
+ runAt: now(),
190
+ idempotencyKey: job.idempotencyKey + ":now",
191
+ });
192
+ session.scheduledMessage = projection(updated);
193
+ scheduler.tick();
194
+ return true;
195
+ } catch (error) {
196
+ visibleError(session, targetWs, "Could not send the scheduled message: " + error.message);
197
+ return false;
198
+ }
199
+ }
200
+
201
+ function canRun(job) {
202
+ var session = findSession(job.target && job.target.id);
203
+ if (!session) return true;
204
+ return !session.isProcessing && !session._queryStarting;
205
+ }
206
+
207
+ function failJob(session, job, error, processingOwner) {
208
+ var ownsProcessing = session && processingOwner && session._scheduledMessageProcessing === processingOwner &&
209
+ session._queryGeneration === processingOwner.queryGeneration &&
210
+ session.queryInstance === processingOwner.queryInstance && !session._queryStarting;
211
+ if (ownsProcessing && session.isProcessing) {
212
+ session.isProcessing = false;
213
+ ctx.onProcessingChanged();
214
+ ctx.sendToSession(session.localId, { type: "status", status: "idle" });
215
+ sm.broadcastSessionList();
216
+ }
217
+ if (session && session._scheduledMessageProcessing === processingOwner) delete session._scheduledMessageProcessing;
218
+ if (session && session.scheduledMessage && session.scheduledMessage.jobId === job.id) {
219
+ session.scheduledMessage = null;
220
+ session.rateLimitAutoContinuePending = false;
221
+ sm.sendAndRecord(session, { type: "scheduled_message_cancelled", jobId: job.id, revision: job.payload.revision, reason: error.message });
222
+ }
223
+ if (session) sm.sendAndRecord(session, { type: "error", text: "Scheduled message failed: " + error.message });
224
+ }
225
+
226
+ async function dispatch(job, execution) {
227
+ var session = findSession(job.target.id);
228
+ var processingOwner = null;
229
+ try {
230
+ if (!session) throw new Error("The scheduled session was deleted.");
231
+ var ownerId = authorize(session, job.payload.actorId);
232
+ if (ownerId !== job.ownerId || ownerId !== job.payload.sessionOwnerId) throw new Error("The session owner changed.");
233
+ if (ctx.osUsers) {
234
+ var owner = users.findUserById(job.ownerId);
235
+ if (!owner || !owner.linuxUser) throw new Error("The session owner has no OS identity.");
236
+ }
237
+ var linuxUser = ctx.ensureProjectAccessForSession(session);
238
+ if (ctx.osUsers && !linuxUser) throw new Error("The session owner OS identity is unavailable.");
239
+ 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;
263
+ }
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 };
269
+ } catch (error) {
270
+ failJob(session, job, error, processingOwner);
271
+ throw error;
272
+ }
273
+ }
274
+
275
+ function rehydrate() {
276
+ sm.sessions.forEach(function (session) { session.scheduledMessage = projection(pendingJob(session)); });
277
+ }
278
+
279
+ function deleted(localId, session) {
280
+ var job = pendingJob(session);
281
+ if (!job) return;
282
+ try { scheduler.cancel(job.id, "session_deleted"); } catch (error) {}
283
+ session.scheduledMessage = null;
284
+ }
285
+
286
+ function notify() {
287
+ if (!scheduler || stopped) return;
288
+ try { scheduler.tick(); } catch (error) {}
289
+ }
290
+
291
+ function shutdown() {
292
+ stopped = true;
293
+ if (unregister) unregister();
294
+ unregister = null;
295
+ }
296
+
297
+ if (scheduler) {
298
+ unregister = scheduler.registerHandler(type, dispatch, { canRun: canRun });
299
+ rehydrate();
300
+ sm.addOnSessionDeleted(deleted);
301
+ sm.addOnSessionViewed(function (session, targetWs, sendFn) { sendState(session, targetWs, sendFn); });
302
+ }
303
+
304
+ return {
305
+ schedule: schedule,
306
+ cancel: cancel,
307
+ sendNow: sendNow,
308
+ sendState: sendState,
309
+ rehydrate: rehydrate,
310
+ notify: notify,
311
+ shutdown: shutdown,
312
+ handlerType: type,
313
+ };
314
+ }
315
+
316
+ module.exports = { attachProjectScheduledMessages: attachProjectScheduledMessages, LOCAL_OWNER: LOCAL_OWNER };