clay-server 4.0.1-beta.1 → 4.1.0-beta.2

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 (46) hide show
  1. package/lib/daemon-project-access.js +118 -0
  2. package/lib/daemon-projects.js +19 -5
  3. package/lib/daemon.js +19 -18
  4. package/lib/project-logs-access-scope.js +94 -0
  5. package/lib/project-logs-query.js +3 -0
  6. package/lib/project-logs-service.js +86 -84
  7. package/lib/project-logs-store.js +5 -3
  8. package/lib/project-pair-lifecycle.js +140 -102
  9. package/lib/project-pair-message.js +50 -0
  10. package/lib/project-pair-replacement-state.js +72 -0
  11. package/lib/project-pair-task-control.js +264 -0
  12. package/lib/project-pair-usage.js +76 -0
  13. package/lib/project-session-pair.js +115 -116
  14. package/lib/project-sessions.js +25 -1
  15. package/lib/project-worker-proposal.js +69 -58
  16. package/lib/project.js +21 -4
  17. package/lib/public/css/icon-strip.css +40 -0
  18. package/lib/public/modules/app-messages.js +9 -0
  19. package/lib/public/modules/project-access.js +141 -0
  20. package/lib/public/modules/project-grouping.js +22 -0
  21. package/lib/public/modules/sidebar-projects.js +14 -171
  22. package/lib/public/modules/worker-proposal.js +4 -0
  23. package/lib/sdk-bridge.js +20 -0
  24. package/lib/server-admin.js +3 -3
  25. package/lib/server-global-ws.js +10 -0
  26. package/lib/server-mates.js +4 -3
  27. package/lib/server-palette.js +17 -14
  28. package/lib/server.js +184 -102
  29. package/lib/session-pair-factory.js +3 -0
  30. package/lib/session-pair-history.js +40 -0
  31. package/lib/session-pair-mcp-server.js +19 -2
  32. package/lib/session-pair-operation.js +11 -0
  33. package/lib/session-pair-prompts.js +24 -2
  34. package/lib/session-pair-turn-control.js +8 -3
  35. package/lib/session-provenance.js +1 -0
  36. package/lib/session-spawn-mcp-server.js +2 -1
  37. package/lib/sessions.js +2 -0
  38. package/lib/worker-proposal-control.js +69 -0
  39. package/lib/worker-proposal-decision.js +7 -0
  40. package/lib/worker-runtime-catalog.js +52 -0
  41. package/lib/workspace-query-access.js +3 -2
  42. package/lib/workspace-query-service.js +1 -1
  43. package/lib/worktree.js +8 -4
  44. package/lib/ws-schema.js +1 -0
  45. package/lib/yoke/adapters/codex.js +25 -7
  46. package/package.json +1 -1
@@ -0,0 +1,264 @@
1
+ var crypto = require("crypto");
2
+ var buildShape = require("./session-spawn-mcp-server").buildShape;
3
+
4
+ var OUTCOMES = ["completed", "partial", "failed", "interrupted"];
5
+ var MAX_QUEUE = 20;
6
+ var MAX_ITEMS = 50;
7
+ var MAX_TEXT = 1000;
8
+
9
+ function result(value) { return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] }); }
10
+ function cleanText(value, max) {
11
+ var text = typeof value === "string" ? value.trim() : "";
12
+ return text.length > max ? text.slice(0, max) : text;
13
+ }
14
+ function cleanId(value, prefix) {
15
+ if (value === undefined || value === null || value === "") return prefix + crypto.randomUUID();
16
+ if (typeof value !== "string") return null;
17
+ var id = value.trim();
18
+ return id && id.length <= 120 && /^[A-Za-z0-9._:-]+$/.test(id) ? id : null;
19
+ }
20
+ function cleanStrings(values) {
21
+ if (!Array.isArray(values)) return null;
22
+ return values.slice(0, MAX_ITEMS).map(function (value) { return cleanText(value, MAX_TEXT); }).filter(Boolean);
23
+ }
24
+ function cleanVerifications(values) {
25
+ if (!Array.isArray(values)) return null;
26
+ return values.slice(0, MAX_ITEMS).map(function (value) {
27
+ if (typeof value === "string") return { command: cleanText(value, MAX_TEXT), result: "unknown" };
28
+ if (!value || typeof value !== "object") return null;
29
+ return { command: cleanText(value.command, MAX_TEXT), result: cleanText(value.result, MAX_TEXT) || "unknown" };
30
+ }).filter(Boolean);
31
+ }
32
+
33
+ function attachPairTaskControl(ctx) {
34
+ function queueFor(worker) {
35
+ if (!Array.isArray(worker._pairFollowups)) worker._pairFollowups = [];
36
+ return worker._pairFollowups;
37
+ }
38
+ function registryFor(worker) {
39
+ if (!worker._pairTaskIds) worker._pairTaskIds = Object.create(null);
40
+ return worker._pairTaskIds;
41
+ }
42
+ function current(worker) { return worker && worker._pairDelegation || null; }
43
+ function taskGeneration(worker) {
44
+ return worker._pairGeneration || worker.sessionProvenance && worker.sessionProvenance.generation || null;
45
+ }
46
+ function findTask(worker, taskId) {
47
+ var active = current(worker);
48
+ if (active && active.taskId === taskId) return active;
49
+ var queue = queueFor(worker);
50
+ for (var i = 0; i < queue.length; i++) if (queue[i].taskId === taskId) return queue[i];
51
+ return null;
52
+ }
53
+ function begin(worker, caller, message, requestedId) {
54
+ var taskId = cleanId(requestedId, "ptask_");
55
+ if (!taskId) throw new Error("taskId is invalid");
56
+ var existing = registryFor(worker)[taskId];
57
+ if (existing && existing.status === "starting" && existing.workerSessionId === worker.localId &&
58
+ existing.driverSessionId === caller.localId && existing.message === message) {
59
+ existing.status = "running";
60
+ existing.startedAt = existing.startedAt || Date.now();
61
+ return existing;
62
+ }
63
+ if (findTask(worker, taskId) || existing) throw new Error("taskId is already in use for this Worker generation");
64
+ var task = {
65
+ taskId: taskId,
66
+ generation: taskGeneration(worker),
67
+ workerSessionId: worker.localId,
68
+ driverSessionId: caller.localId,
69
+ message: message,
70
+ createdAt: Date.now(),
71
+ status: "running",
72
+ };
73
+ registryFor(worker)[taskId] = task;
74
+ return task;
75
+ }
76
+ function projectTask(task) {
77
+ return {
78
+ taskId: task.taskId,
79
+ generation: task.generation,
80
+ workerSessionId: task.workerSessionId,
81
+ status: task.status,
82
+ createdAt: task.createdAt,
83
+ startedAt: task.startedAt || null,
84
+ replacesTaskId: task.replacesTaskId || null,
85
+ message: cleanText(task.message, MAX_TEXT),
86
+ };
87
+ }
88
+ function queue(args, caller) {
89
+ try {
90
+ ctx.turnControl.assertWorkerAction(caller);
91
+ var resolved = ctx.resolvePair(caller);
92
+ var worker = resolved.partner;
93
+ var message = cleanText(args.message, 30000);
94
+ if (!message) throw new Error("message is required");
95
+ if (!worker.isProcessing && !worker._queryStarting && !current(worker)) throw new Error("the Split Worker is idle; use send_to_partner for an immediate follow-up");
96
+ var task = begin(worker, caller, message, args.taskId);
97
+ task.status = "queued";
98
+ var items = queueFor(worker);
99
+ if (items.filter(function (item) { return item.status === "queued" || item.status === "waiting_for_interrupt"; }).length >= MAX_QUEUE) throw new Error("the Split Worker follow-up queue is full");
100
+ items.push(task);
101
+ return result({ status: "queued", task: projectTask(task) });
102
+ } catch (err) { return result({ status: "rejected", reason: err.message || String(err) }); }
103
+ }
104
+ function inspect(args, caller) {
105
+ try {
106
+ var worker = ctx.resolvePair(caller).partner;
107
+ return result(status(worker));
108
+ } catch (err) { return result({ status: "rejected", reason: err.message || String(err) }); }
109
+ }
110
+ function cancel(args, caller) {
111
+ try {
112
+ ctx.turnControl.assertWorkerAction(caller);
113
+ var worker = ctx.resolvePair(caller).partner;
114
+ var task = findTask(worker, args.taskId);
115
+ if (!task || (task.status !== "queued" && task.status !== "waiting_for_interrupt")) throw new Error("the exact queued task is not cancellable");
116
+ task.status = "cancelled";
117
+ task.completedAt = Date.now();
118
+ return result({ status: "cancelled", taskId: task.taskId });
119
+ } catch (err) { return result({ status: "rejected", taskId: args.taskId || null, reason: err.message || String(err) }); }
120
+ }
121
+ function markInterruption(worker, by, reason, targetTaskId) {
122
+ var active = current(worker);
123
+ worker._pairInterruption = {
124
+ interruptedBy: by || "system",
125
+ reason: cleanText(reason, MAX_TEXT) || "No reason supplied.",
126
+ targetTaskId: targetTaskId || active && active.taskId || null,
127
+ requestedAt: Date.now(),
128
+ completedAt: null,
129
+ };
130
+ return worker._pairInterruption;
131
+ }
132
+ function ensureSystemInterruption(worker, token) {
133
+ if (worker._lastTurnInterrupted && (!worker._pairInterruption || worker._pairInterruption.targetTaskId !== token.taskId)) {
134
+ markInterruption(worker, "system", "The Worker runtime ended this task as interrupted.", token.taskId);
135
+ }
136
+ }
137
+ function replaceTask(args, caller) {
138
+ try {
139
+ ctx.turnControl.assertWorkerAction(caller);
140
+ var resolved = ctx.resolvePair(caller);
141
+ var worker = resolved.partner;
142
+ var active = current(worker);
143
+ if (!active || !worker.isProcessing) throw new Error("there is no active delegated task to replace");
144
+ if (!args.targetTaskId || args.targetTaskId !== active.taskId) throw new Error("targetTaskId must match the exact active task");
145
+ var message = cleanText(args.message, 30000);
146
+ if (!message) throw new Error("message is required");
147
+ var replacement = begin(worker, caller, message, args.taskId);
148
+ replacement.status = "waiting_for_interrupt";
149
+ replacement.replacesTaskId = active.taskId;
150
+ queueFor(worker).push(replacement);
151
+ markInterruption(worker, "driver", args.reason || "The Driver replaced the active task.", active.taskId);
152
+ worker.taskStopRequested = true;
153
+ if (worker.abortController) worker.abortController.abort();
154
+ return result({ status: "interrupting", targetTaskId: active.taskId, replacement: projectTask(replacement) });
155
+ } catch (err) { return result({ status: "rejected", reason: err.message || String(err) }); }
156
+ }
157
+ function resumeTask(args, caller) {
158
+ try {
159
+ ctx.turnControl.assertWorkerAction(caller);
160
+ var worker = ctx.resolvePair(caller).partner;
161
+ if (worker.isProcessing || worker._queryStarting || current(worker)) throw new Error("the Split Worker is not idle");
162
+ var prior = worker._lastInterruptedPairTask;
163
+ if (!prior) throw new Error("the Split Worker has no resumable interrupted task");
164
+ var message = cleanText(args.message, 30000) || prior.message;
165
+ return ctx.sendFollowup({ message: message, wait: args.wait, timeoutSeconds: args.timeoutSeconds, taskId: args.taskId }, caller);
166
+ } catch (err) { return result({ status: "rejected", reason: err.message || String(err) }); }
167
+ }
168
+ function report(args, worker) {
169
+ try {
170
+ if (!worker || ctx.sm.sessions.get(worker.localId) !== worker) throw new Error("the Worker session is no longer live");
171
+ var resolved = ctx.rolesFor(worker);
172
+ if (!resolved || resolved.worker !== worker) throw new Error("only the exact configured Split Worker can report an outcome");
173
+ var active = current(worker);
174
+ if (!active || args.taskId !== active.taskId) throw new Error("taskId must match the exact active delegated task");
175
+ var outcome = typeof args.outcome === "string" ? args.outcome.trim().toLowerCase() : "";
176
+ if (OUTCOMES.indexOf(outcome) === -1) throw new Error("outcome must be completed, partial, failed, or interrupted");
177
+ active.reportedOutcome = {
178
+ outcome: outcome,
179
+ summary: cleanText(args.summary, MAX_TEXT) || null,
180
+ changedFiles: cleanStrings(args.changedFiles),
181
+ verifications: cleanVerifications(args.verifications),
182
+ unverified: cleanStrings(args.unverified),
183
+ processes: cleanStrings(args.processes),
184
+ nextAction: cleanText(args.nextAction, MAX_TEXT) || null,
185
+ reportedAt: Date.now(),
186
+ };
187
+ return result({ status: "recorded", taskId: active.taskId });
188
+ } catch (err) { return result({ status: "rejected", reason: err.message || String(err) }); }
189
+ }
190
+ function complete(worker, caller, token, observedStatus, response, error) {
191
+ if (!token) return null;
192
+ if (token.outcomeEnvelope) return token.outcomeEnvelope;
193
+ var interruption = worker._pairInterruption && worker._pairInterruption.targetTaskId === token.taskId ? worker._pairInterruption : null;
194
+ if (interruption) interruption.completedAt = Date.now();
195
+ var report = token.reportedOutcome || null;
196
+ var finalStatus = error ? "failed" : (interruption ? "interrupted" : (report ? report.outcome : observedStatus));
197
+ var envelope = {
198
+ taskId: token.taskId,
199
+ generation: token.generation,
200
+ workerSessionId: worker.localId,
201
+ status: finalStatus,
202
+ completedAt: Date.now(),
203
+ interruption: interruption,
204
+ workerReport: report,
205
+ changedFiles: report ? report.changedFiles : null,
206
+ verifications: report ? report.verifications : null,
207
+ unverified: report ? report.unverified : null,
208
+ processes: report ? report.processes : null,
209
+ nextAction: report ? report.nextAction : null,
210
+ verificationAuthority: report ? "worker_reported" : "unknown",
211
+ response: response || "",
212
+ error: error || null,
213
+ };
214
+ token.status = finalStatus;
215
+ token.outcomeEnvelope = envelope;
216
+ worker._lastPairOutcome = envelope;
217
+ if (finalStatus === "interrupted" || finalStatus === "partial") worker._lastInterruptedPairTask = { taskId: token.taskId, message: token.message, generation: token.generation };
218
+ if (ctx.sm.sendToSession) ctx.sm.sendToSession(caller, { type: "partner_task_completed", outcome: envelope });
219
+ return envelope;
220
+ }
221
+ function status(worker) {
222
+ var active = current(worker);
223
+ var registry = registryFor(worker);
224
+ var ids = Object.keys(registry);
225
+ return {
226
+ status: "ok",
227
+ current: active ? projectTask(active) : null,
228
+ queued: queueFor(worker).filter(function (item) { return item.status === "queued" || item.status === "waiting_for_interrupt"; }).map(projectTask),
229
+ interruption: worker._pairInterruption || null,
230
+ lastOutcome: worker._lastPairOutcome || null,
231
+ recent: ids.slice(-20).map(function (id) { return projectTask(registry[id]); }),
232
+ };
233
+ }
234
+ function drain(caller, worker) {
235
+ if (ctx.turnControl.blockedReason(caller) || worker.isProcessing || worker._queryStarting || current(worker)) return false;
236
+ var queue = queueFor(worker);
237
+ var next = null;
238
+ for (var i = 0; i < queue.length; i++) {
239
+ if (queue[i].status === "queued" || queue[i].status === "waiting_for_interrupt") { next = queue[i]; break; }
240
+ }
241
+ if (!next) return false;
242
+ next.status = "starting";
243
+ next.startedAt = Date.now();
244
+ queue.splice(queue.indexOf(next), 1);
245
+ Promise.resolve(ctx.sendFollowup({ message: next.message, wait: false, taskId: next.taskId }, caller)).then(function (delivery) {
246
+ next.status = delivery && delivery.isError ? "failed" : "running";
247
+ if (delivery && delivery.isError) next.error = delivery.content && delivery.content[0] && delivery.content[0].text || "Follow-up rejected";
248
+ }, function (err) { next.status = "failed"; next.error = err.message || String(err); });
249
+ return true;
250
+ }
251
+ function driverToolDefs(handlers) {
252
+ return [{ name: "queue_partner_followup", description: "Queue a distinct follow-up task behind the active Split Worker task.", inputSchema: buildShape({ message: { type: "string" }, taskId: { type: "string" } }, ["message"]), handler: handlers.queue },
253
+ { name: "inspect_partner_followups", description: "Inspect current, queued, interrupted, and last completed Split Worker task state.", inputSchema: buildShape({}), handler: handlers.inspect },
254
+ { name: "cancel_partner_followup", description: "Cancel one exact queued follow-up without interrupting active work.", inputSchema: buildShape({ taskId: { type: "string" } }, ["taskId"]), handler: handlers.cancel },
255
+ { name: "replace_partner_task", description: "Interrupt one exact active delegated task and queue a replacement task in the same Worker generation.", inputSchema: buildShape({ targetTaskId: { type: "string" }, message: { type: "string" }, taskId: { type: "string" }, reason: { type: "string" } }, ["targetTaskId", "message"]), handler: handlers.replace },
256
+ { name: "resume_partner_task", description: "Resume the last interrupted task as a new correlated task. A human Stop blocks this until a new user turn.", inputSchema: buildShape({ message: { type: "string" }, taskId: { type: "string" }, wait: { type: "boolean" }, timeoutSeconds: { type: "number" } }), handler: handlers.resume }];
257
+ }
258
+ function workerToolDefs(worker) {
259
+ return [{ name: "report_partner_outcome", description: "Report structured, Worker-observed task outcome data. This does not claim Driver verification.", inputSchema: buildShape({ taskId: { type: "string" }, outcome: { type: "string" }, summary: { type: "string" }, changedFiles: { type: "array", items: { type: "string" } }, verifications: { type: "array", items: { type: "object" } }, unverified: { type: "array", items: { type: "string" } }, processes: { type: "array", items: { type: "string" } }, nextAction: { type: "string" } }, ["taskId", "outcome"]), handler: function (args) { return report(args || {}, worker); } }];
260
+ }
261
+ return { begin: begin, cancel: cancel, complete: complete, drain: drain, driverToolDefs: driverToolDefs, ensureSystemInterruption: ensureSystemInterruption, inspect: inspect, markInterruption: markInterruption, queue: queue, replaceTask: replaceTask, resumeTask: resumeTask, status: status, workerToolDefs: workerToolDefs };
262
+ }
263
+
264
+ module.exports = { attachPairTaskControl: attachPairTaskControl };
@@ -0,0 +1,76 @@
1
+ function firstNumber() {
2
+ for (var i = 0; i < arguments.length; i++) {
3
+ var value = arguments[i];
4
+ if (typeof value === "number" && isFinite(value) && value >= 0) return value;
5
+ }
6
+ return null;
7
+ }
8
+
9
+ function matchingWindow(modelUsage, model) {
10
+ if (!modelUsage || !model || !modelUsage[model]) return null;
11
+ return firstNumber(modelUsage[model].contextWindow);
12
+ }
13
+
14
+ function contextStatus(session) {
15
+ var snapshot = session.lastContextUsage || null;
16
+ var used = null;
17
+ var window = null;
18
+ var source = "unavailable";
19
+ if (snapshot && typeof snapshot === "object") {
20
+ if (session.vendor === "claude") {
21
+ used = firstNumber(snapshot.totalTokens);
22
+ window = firstNumber(snapshot.maxTokens);
23
+ if (used !== null || window !== null) source = "claude_sdk_context_usage";
24
+ } else {
25
+ used = firstNumber(snapshot.input_tokens);
26
+ window = firstNumber(snapshot.contextWindow);
27
+ if (used !== null || window !== null) source = "adapter_context_usage";
28
+ }
29
+ }
30
+ var history = session.history || [];
31
+ var cumulativeInput = 0;
32
+ var cumulativeOutput = 0;
33
+ var results = 0;
34
+ var observedInput = false;
35
+ var observedOutput = false;
36
+ var lastTask = null;
37
+ var compactions = 0;
38
+ var compacting = false;
39
+ var lastCompactionAt = null;
40
+ for (var i = 0; i < history.length; i++) {
41
+ var item = history[i];
42
+ if (!item) continue;
43
+ if (item.type === "compacting") {
44
+ if (item.active) compactions++;
45
+ compacting = !!item.active;
46
+ if (typeof item._ts === "number") lastCompactionAt = item._ts;
47
+ }
48
+ if (item.type !== "result") continue;
49
+ var usage = item.usage || {};
50
+ var input = firstNumber(usage.input_tokens, usage.inputTokens);
51
+ var output = firstNumber(usage.output_tokens, usage.outputTokens);
52
+ if (input !== null) { cumulativeInput += input; observedInput = true; }
53
+ if (output !== null) { cumulativeOutput += output; observedOutput = true; }
54
+ results++;
55
+ lastTask = { inputTokens: input, outputTokens: output, currentInputTokens: null };
56
+ var observedWindow = matchingWindow(item.modelUsage, session.model);
57
+ if (observedWindow !== null) window = observedWindow;
58
+ }
59
+ if (used !== null && window !== null && used > window) {
60
+ used = null;
61
+ source = "unavailable";
62
+ }
63
+ // Older result records contain lastStreamInputTokens, but that field was
64
+ // populated from adapter snapshots that could be cumulative or inflated by
65
+ // cache accounting. It is not safe evidence of current context occupancy.
66
+ var ratio = used !== null && window !== null && window > 0 ? Math.min(1, Math.round((used / window) * 1000) / 1000) : null;
67
+ return {
68
+ current: { source: source, usedTokens: used, windowTokens: window, usedRatio: ratio,
69
+ scope: used === null && window === null ? "unknown" : (used !== null && window !== null ? "current_context" : "partial_current_context") },
70
+ cumulative: { inputTokens: observedInput ? cumulativeInput : null, outputTokens: observedOutput ? cumulativeOutput : null, tasksObserved: results, scope: "loaded_session_history", complete: false },
71
+ lastTask: lastTask,
72
+ compactions: { observedCount: compactions, active: compacting, lastObservedAt: lastCompactionAt, scope: "loaded_session_history", complete: false },
73
+ };
74
+ }
75
+
76
+ module.exports = { contextStatus: contextStatus, firstNumber: firstNumber };