clay-server 4.0.0 → 4.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/daemon-project-access.js +118 -0
- package/lib/daemon-projects.js +19 -5
- package/lib/daemon.js +19 -18
- package/lib/project-logs-access-scope.js +94 -0
- package/lib/project-logs-query.js +3 -0
- package/lib/project-logs-service.js +86 -84
- package/lib/project-logs-store.js +5 -3
- package/lib/project-pair-lifecycle.js +71 -61
- package/lib/project-pair-message.js +50 -0
- package/lib/project-pair-replacement-state.js +72 -0
- package/lib/project-pair-task-control.js +264 -0
- package/lib/project-pair-usage.js +73 -0
- package/lib/project-session-pair.js +115 -116
- package/lib/project-sessions.js +40 -6
- package/lib/project-worker-proposal.js +54 -57
- package/lib/project.js +27 -4
- package/lib/public/app.js +1 -0
- package/lib/public/css/command-palette.css +26 -0
- package/lib/public/css/icon-strip.css +40 -0
- package/lib/public/index.html +2 -2
- package/lib/public/modules/app-message-router.js +1 -1
- package/lib/public/modules/app-messages.js +9 -0
- package/lib/public/modules/project-access.js +141 -0
- package/lib/public/modules/project-grouping.js +22 -0
- package/lib/public/modules/search-clay-chat.js +47 -6
- package/lib/public/modules/search-clay-controls.js +86 -0
- package/lib/public/modules/sidebar-projects.js +14 -171
- package/lib/public/modules/worker-proposal.js +4 -0
- package/lib/public/style.css +1 -1
- package/lib/sdk-bridge.js +21 -1
- package/lib/server-admin.js +3 -3
- package/lib/server-global-ws.js +10 -0
- package/lib/server-home-chat-events.js +3 -0
- package/lib/server-home-chat.js +35 -0
- package/lib/server-mates.js +4 -3
- package/lib/server-palette.js +17 -14
- package/lib/server.js +184 -102
- package/lib/session-pair-factory.js +3 -0
- package/lib/session-pair-history.js +40 -0
- package/lib/session-pair-mcp-server.js +12 -1
- package/lib/session-pair-operation.js +11 -0
- package/lib/session-pair-prompts.js +24 -2
- package/lib/session-pair-turn-control.js +8 -3
- package/lib/worker-proposal-control.js +69 -0
- package/lib/worker-proposal-decision.js +7 -0
- package/lib/worker-runtime-catalog.js +52 -0
- package/lib/workspace-query-access.js +3 -2
- package/lib/workspace-query-service.js +1 -1
- package/lib/worktree.js +8 -4
- package/lib/ws-schema.js +7 -0
- package/lib/yoke/adapters/codex.js +6 -2
- package/package.json +1 -1
|
@@ -0,0 +1,73 @@
|
|
|
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: firstNumber(item.lastStreamInputTokens) };
|
|
56
|
+
var observedWindow = matchingWindow(item.modelUsage, session.model);
|
|
57
|
+
if (observedWindow !== null) window = observedWindow;
|
|
58
|
+
}
|
|
59
|
+
if (used === null && lastTask && lastTask.currentInputTokens !== null) {
|
|
60
|
+
used = lastTask.currentInputTokens;
|
|
61
|
+
source = "last_stream_input";
|
|
62
|
+
}
|
|
63
|
+
var ratio = used !== null && window !== null && window > 0 ? Math.min(1, Math.round((used / window) * 1000) / 1000) : null;
|
|
64
|
+
return {
|
|
65
|
+
current: { source: source, usedTokens: used, windowTokens: window, usedRatio: ratio,
|
|
66
|
+
scope: used === null && window === null ? "unknown" : (used !== null && window !== null ? "current_context" : "partial_current_context") },
|
|
67
|
+
cumulative: { inputTokens: observedInput ? cumulativeInput : null, outputTokens: observedOutput ? cumulativeOutput : null, tasksObserved: results, scope: "loaded_session_history", complete: false },
|
|
68
|
+
lastTask: lastTask,
|
|
69
|
+
compactions: { observedCount: compactions, active: compacting, lastObservedAt: lastCompactionAt, scope: "loaded_session_history", complete: false },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { contextStatus: contextStatus, firstNumber: firstNumber };
|
|
@@ -1,69 +1,24 @@
|
|
|
1
|
-
var pairMcp = require("./session-pair-mcp-server");
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
var { attachPairLifecycle } = require("./project-pair-lifecycle");
|
|
5
|
-
var { attachPairTurnControl } = require("./session-pair-turn-control");
|
|
1
|
+
var pairMcp = require("./session-pair-mcp-server"), pairPrompts = require("./session-pair-prompts");
|
|
2
|
+
var { attachPairFactory } = require("./session-pair-factory"), { attachPairLifecycle } = require("./project-pair-lifecycle"),
|
|
3
|
+
{ attachPairTurnControl } = require("./session-pair-turn-control");
|
|
6
4
|
var driverEligibility = require("./session-driver-eligibility"), driverOrchestration = require("./session-driver-orchestration");
|
|
7
|
-
var { attachWorkerPermission } = require("./project-worker-permission");
|
|
8
|
-
var {
|
|
9
|
-
var
|
|
5
|
+
var { attachWorkerPermission } = require("./project-worker-permission"), { attachWorkerProposal } = require("./project-worker-proposal");
|
|
6
|
+
var { attachPairMessage } = require("./project-pair-message"), { attachPairTaskControl } = require("./project-pair-task-control");
|
|
7
|
+
var pairHistory = require("./session-pair-history"), responseText = pairHistory.responseText, errorSince = pairHistory.errorSince, recentTurns = pairHistory.recentTurns, operationFingerprint = require("./session-pair-operation").fingerprint;
|
|
10
8
|
function toolResult(value) { return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] }); }
|
|
11
9
|
function toolError(err) {
|
|
12
|
-
return Promise.resolve({
|
|
13
|
-
content: [{ type: "text", text: "Error: " + (err.message || err) }],
|
|
14
|
-
isError: true,
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
function responseText(history, fromIndex) {
|
|
18
|
-
var text = "";
|
|
19
|
-
for (var i = Math.max(0, fromIndex || 0); i < history.length; i++) {
|
|
20
|
-
if (history[i] && history[i].type === "delta" && history[i].text) text += history[i].text;
|
|
21
|
-
}
|
|
22
|
-
if (text.length > MAX_RESPONSE_CHARS) text = text.slice(-MAX_RESPONSE_CHARS);
|
|
23
|
-
return text;
|
|
10
|
+
return Promise.resolve({ content: [{ type: "text", text: "Error: " + (err.message || err) }], isError: true });
|
|
24
11
|
}
|
|
25
|
-
|
|
26
|
-
function errorSince(history, fromIndex) {
|
|
27
|
-
for (var i = history.length - 1; i >= Math.max(0, fromIndex || 0); i--) {
|
|
28
|
-
if (history[i] && history[i].type === "error") return history[i].text || "partner turn failed";
|
|
29
|
-
}
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function recentTurns(session, count) {
|
|
34
|
-
var history = session.history || [];
|
|
35
|
-
var starts = [];
|
|
36
|
-
for (var i = 0; i < history.length; i++) {
|
|
37
|
-
if (history[i] && history[i].type === "user_message") starts.push(i);
|
|
38
|
-
}
|
|
39
|
-
var from = starts.length > 0 ? starts[Math.max(0, starts.length - count)] : 0;
|
|
40
|
-
var turns = [];
|
|
41
|
-
var current = null;
|
|
42
|
-
for (var j = from; j < history.length; j++) {
|
|
43
|
-
var item = history[j];
|
|
44
|
-
if (!item) continue;
|
|
45
|
-
if (item.type === "user_message") {
|
|
46
|
-
current = { user: item.text || "", delegated: !!item.delegated, response: "" };
|
|
47
|
-
turns.push(current);
|
|
48
|
-
} else if (item.type === "delta" && item.text) {
|
|
49
|
-
if (!current) { current = { user: "", delegated: false, response: "" }; turns.push(current); }
|
|
50
|
-
current.response += item.text;
|
|
51
|
-
if (current.response.length > MAX_RESPONSE_CHARS) current.response = current.response.slice(-MAX_RESPONSE_CHARS);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return turns;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
12
|
function attachSessionPair(ctx) {
|
|
58
13
|
var sm = ctx.sm;
|
|
59
14
|
var store = ctx.splitStore;
|
|
60
15
|
var workerProposal;
|
|
61
16
|
var workerPermission;
|
|
62
17
|
var lifecycle;
|
|
18
|
+
var taskControl;
|
|
63
19
|
var turnControl = attachPairTurnControl({ sm: sm, splitStore: store });
|
|
64
20
|
function groupAndPartner(caller, missingPairMessage) {
|
|
65
21
|
if (!caller) throw new Error("partner tools require a session-bound tool server");
|
|
66
|
-
// Captured handlers must remain bound to the exact live Session object.
|
|
67
22
|
if (sm.sessions.get(caller.localId) !== caller) {
|
|
68
23
|
throw new Error("this session is no longer live; the partner tools are bound to an exact session");
|
|
69
24
|
}
|
|
@@ -78,26 +33,42 @@ function attachSessionPair(ctx) {
|
|
|
78
33
|
if (caller.ownerId !== partner.ownerId) throw new Error("split partner access denied");
|
|
79
34
|
return { group: group, partner: partner };
|
|
80
35
|
}
|
|
81
|
-
|
|
36
|
+
function rolesFor(session) {
|
|
37
|
+
if (!session || sm.sessions.get(session.localId) !== session) return null;
|
|
38
|
+
var group = store.groupForMember(session.localId);
|
|
39
|
+
if (!group || !group.pair) return null;
|
|
40
|
+
var driver = sm.sessions.get(group.pair.driverId);
|
|
41
|
+
var worker = sm.sessions.get(group.pair.workerId);
|
|
42
|
+
if (!driver || !worker || driver.ownerId !== worker.ownerId) return null;
|
|
43
|
+
return { group: group, driver: driver, worker: worker };
|
|
44
|
+
}
|
|
45
|
+
var pairMessage = attachPairMessage({
|
|
46
|
+
sm: sm,
|
|
47
|
+
getSdk: ctx.getSdk,
|
|
48
|
+
resolvePair: groupAndPartner,
|
|
49
|
+
turnControl: turnControl,
|
|
50
|
+
});
|
|
51
|
+
taskControl = attachPairTaskControl({
|
|
52
|
+
sm: sm, resolvePair: groupAndPartner, rolesFor: rolesFor, turnControl: turnControl,
|
|
53
|
+
sendFollowup: function (args, caller) { return sendToPartner(args, caller); },
|
|
54
|
+
});
|
|
82
55
|
function broadcastDelegation(group, caller, partner, active) {
|
|
83
|
-
var message = {
|
|
84
|
-
|
|
85
|
-
groupId: group.id,
|
|
86
|
-
from: caller.localId,
|
|
87
|
-
to: partner.localId,
|
|
88
|
-
active: !!active,
|
|
89
|
-
};
|
|
56
|
+
var message = { type: "split_delegation", groupId: group.id, from: caller.localId,
|
|
57
|
+
to: partner.localId, active: !!active };
|
|
90
58
|
if (typeof ctx.broadcastDelegation === "function") ctx.broadcastDelegation(group, message);
|
|
91
59
|
else ctx.send(message);
|
|
92
60
|
}
|
|
93
|
-
|
|
94
61
|
function finishDelegation(group, caller, partner, token) {
|
|
95
62
|
if (partner._pairDelegation !== token) return;
|
|
96
63
|
delete partner._pairDelegation;
|
|
97
64
|
delete partner._delegatedBy;
|
|
98
65
|
broadcastDelegation(group, caller, partner, false);
|
|
99
66
|
}
|
|
100
|
-
|
|
67
|
+
function completeInterruptedTask(partner, caller, token) {
|
|
68
|
+
if (!token) return null;
|
|
69
|
+
return taskControl.complete(partner, caller, token, "interrupted",
|
|
70
|
+
responseText(partner.history || [], token.startIndex), null);
|
|
71
|
+
}
|
|
101
72
|
function resumeDriverWithMessage(caller, text, meta) {
|
|
102
73
|
if (!caller || caller.destroying) return false;
|
|
103
74
|
if (turnControl.blockedReason(caller)) return false;
|
|
@@ -134,7 +105,6 @@ function attachSessionPair(ctx) {
|
|
|
134
105
|
sm.broadcastSessionList();
|
|
135
106
|
return true;
|
|
136
107
|
}
|
|
137
|
-
|
|
138
108
|
function resumeDriverWithResult(caller, partner, token) {
|
|
139
109
|
if (!token.detached || token.delivered || caller.destroying) return false;
|
|
140
110
|
token.delivered = true;
|
|
@@ -154,14 +124,12 @@ function attachSessionPair(ctx) {
|
|
|
154
124
|
partnerSessionId: partner.localId,
|
|
155
125
|
});
|
|
156
126
|
}
|
|
157
|
-
|
|
158
127
|
function requestDetach(worker) {
|
|
159
128
|
var token = worker && worker._pairDelegation;
|
|
160
129
|
if (!token || token.detached || token.detachRequested) return false;
|
|
161
130
|
token.detachRequested = true;
|
|
162
131
|
return true;
|
|
163
132
|
}
|
|
164
|
-
|
|
165
133
|
function handleTurnDone(partner) {
|
|
166
134
|
var token = partner && partner._pairDelegation;
|
|
167
135
|
if (!token) return false;
|
|
@@ -176,10 +144,13 @@ function attachSessionPair(ctx) {
|
|
|
176
144
|
token.response = responseText(partner.history || [], token.startIndex);
|
|
177
145
|
token.failure = errorSince(partner.history || [], token.startIndex);
|
|
178
146
|
token.interrupted = !token.failure && !!partner._lastTurnInterrupted;
|
|
147
|
+
if (token.interrupted) taskControl.ensureSystemInterruption(partner, token);
|
|
148
|
+
taskControl.complete(partner, caller, token, token.failure ? "failed" : (token.interrupted ? "interrupted" : "completed"), token.response, token.failure);
|
|
179
149
|
finishDelegation(group, caller, partner, token);
|
|
180
|
-
|
|
150
|
+
var resumed = resumeDriverWithResult(caller, partner, token);
|
|
151
|
+
taskControl.drain(caller, partner);
|
|
152
|
+
return resumed;
|
|
181
153
|
}
|
|
182
|
-
|
|
183
154
|
function waitForPartner(group, caller, partner, token, timeoutSeconds) {
|
|
184
155
|
return new Promise(function (resolve, reject) {
|
|
185
156
|
var deadline = Date.now() + timeoutSeconds * 1000;
|
|
@@ -196,24 +167,29 @@ function attachSessionPair(ctx) {
|
|
|
196
167
|
finishDelegation(group, caller, partner, token);
|
|
197
168
|
var failure = errorSince(partner.history || [], token.startIndex);
|
|
198
169
|
var interrupted = !failure && !!partner._lastTurnInterrupted;
|
|
170
|
+
var response = responseText(partner.history || [], token.startIndex);
|
|
171
|
+
if (interrupted) taskControl.ensureSystemInterruption(partner, token);
|
|
172
|
+
var outcome = taskControl.complete(partner, caller, token, failure ? "failed" : (interrupted ? "interrupted" : "completed"), response, failure);
|
|
199
173
|
resolve({
|
|
200
|
-
status: failure ? "error" : (
|
|
201
|
-
|
|
174
|
+
status: failure ? "error" : (outcome.status === "completed" ? "complete" : outcome.status),
|
|
175
|
+
taskId: token.taskId,
|
|
176
|
+
generation: token.generation,
|
|
177
|
+
response: response,
|
|
202
178
|
error: failure || undefined,
|
|
179
|
+
outcome: outcome,
|
|
203
180
|
});
|
|
181
|
+
taskControl.drain(caller, partner);
|
|
204
182
|
return;
|
|
205
183
|
}
|
|
206
|
-
// Timeouts and permission requests both detach into the push-back path.
|
|
207
184
|
if (Date.now() >= deadline || token.detachRequested) {
|
|
208
185
|
clearInterval(timer);
|
|
209
186
|
token.detached = true;
|
|
210
187
|
monitorPartner(group, caller, partner, token);
|
|
211
|
-
resolve({ status: "running", response: responseText(partner.history || [], token.startIndex), hint: "The completed result will be pushed back automatically. Use read_partner only for an interim status check." });
|
|
188
|
+
resolve({ status: "running", taskId: token.taskId, generation: token.generation, response: responseText(partner.history || [], token.startIndex), hint: "The completed result will be pushed back automatically. Use read_partner only for an interim status check." });
|
|
212
189
|
}
|
|
213
190
|
}, 500);
|
|
214
191
|
});
|
|
215
192
|
}
|
|
216
|
-
|
|
217
193
|
function monitorPartner(group, caller, partner, token) {
|
|
218
194
|
var timer = setInterval(function () {
|
|
219
195
|
var currentGroup = store.groupForMember(caller.localId);
|
|
@@ -224,7 +200,6 @@ function attachSessionPair(ctx) {
|
|
|
224
200
|
}
|
|
225
201
|
}, 500);
|
|
226
202
|
}
|
|
227
|
-
|
|
228
203
|
async function sendToPartner(args, caller) {
|
|
229
204
|
try {
|
|
230
205
|
if (caller && caller._delegatedBy) throw new Error("delegated turns cannot delegate to another session");
|
|
@@ -238,7 +213,9 @@ function attachSessionPair(ctx) {
|
|
|
238
213
|
timeout = Math.max(1, Math.min(900, timeout));
|
|
239
214
|
var partner = resolved.partner;
|
|
240
215
|
if (partner._pairDelegation) throw new Error("the partner is already handling a delegated task");
|
|
241
|
-
|
|
216
|
+
delete partner._pairInterruption; delete partner._lastInterruptedPairTask;
|
|
217
|
+
var task = taskControl.begin(partner, caller, message, args.taskId);
|
|
218
|
+
var token = Object.assign(task, { from: caller.localId, groupId: resolved.group.id, startIndex: partner.history.length });
|
|
242
219
|
partner._pairDelegation = token;
|
|
243
220
|
partner._delegatedBy = caller.localId;
|
|
244
221
|
sm.sendAndRecord(partner, {
|
|
@@ -248,6 +225,8 @@ function attachSessionPair(ctx) {
|
|
|
248
225
|
delegatedBy: caller.localId,
|
|
249
226
|
delegatedByTitle: caller.title || "Driver",
|
|
250
227
|
delegatedByVendor: caller.vendor || "claude",
|
|
228
|
+
delegatedTaskId: token.taskId,
|
|
229
|
+
delegatedGeneration: token.generation,
|
|
251
230
|
});
|
|
252
231
|
partner.lastActivity = Date.now();
|
|
253
232
|
partner.sentToolResults = {};
|
|
@@ -259,9 +238,10 @@ function attachSessionPair(ctx) {
|
|
|
259
238
|
ctx.onProcessingChanged();
|
|
260
239
|
sm.sendToSession(partner, { type: "status", status: "processing" });
|
|
261
240
|
}
|
|
262
|
-
|
|
241
|
+
var runtimeMessage = "[Split Worker taskId=" + token.taskId + " generation=" + (token.generation === null ? "unknown" : token.generation) + "]\n" + message + "\n\nBefore finishing, call report_partner_outcome with this exact taskId and only Worker-observed evidence.";
|
|
242
|
+
if (!sdk.pushMessage(partner, runtimeMessage)) {
|
|
263
243
|
partner._queryStartTs = Date.now();
|
|
264
|
-
Promise.resolve(sdk.startQuery(partner,
|
|
244
|
+
Promise.resolve(sdk.startQuery(partner, runtimeMessage, undefined, ctx.getLinuxUserForSession(partner))).catch(function (err) {
|
|
265
245
|
partner.isProcessing = false;
|
|
266
246
|
sm.sendAndRecord(partner, { type: "error", text: err.message || String(err) });
|
|
267
247
|
});
|
|
@@ -270,15 +250,21 @@ function attachSessionPair(ctx) {
|
|
|
270
250
|
if (!wait) {
|
|
271
251
|
token.detached = true;
|
|
272
252
|
monitorPartner(resolved.group, caller, partner, token);
|
|
273
|
-
return toolResult({ status: "running", partnerId: partner.localId, hint: "The completed result will be pushed back automatically. Use read_partner only for an interim status check." });
|
|
253
|
+
return toolResult({ status: "running", partnerId: partner.localId, taskId: token.taskId, generation: token.generation, hint: "The completed result will be pushed back automatically. Use read_partner only for an interim status check." });
|
|
274
254
|
}
|
|
275
255
|
if (!partner.isProcessing && !partner._queryStarting) {
|
|
276
256
|
finishDelegation(resolved.group, caller, partner, token);
|
|
277
257
|
var immediateFailure = errorSince(partner.history || [], token.startIndex);
|
|
258
|
+
var immediateResponse = responseText(partner.history || [], token.startIndex);
|
|
259
|
+
var immediateOutcome = taskControl.complete(partner, caller, token, immediateFailure ? "failed" : "completed", immediateResponse, immediateFailure);
|
|
260
|
+
taskControl.drain(caller, partner);
|
|
278
261
|
return toolResult({
|
|
279
|
-
status: immediateFailure ? "error" : "complete",
|
|
280
|
-
|
|
262
|
+
status: immediateFailure ? "error" : (immediateOutcome.status === "completed" ? "complete" : immediateOutcome.status),
|
|
263
|
+
taskId: token.taskId,
|
|
264
|
+
generation: token.generation,
|
|
265
|
+
response: immediateResponse,
|
|
281
266
|
error: immediateFailure || undefined,
|
|
267
|
+
outcome: immediateOutcome,
|
|
282
268
|
});
|
|
283
269
|
}
|
|
284
270
|
var completed = await waitForPartner(resolved.group, caller, partner, token, timeout);
|
|
@@ -297,7 +283,6 @@ function attachSessionPair(ctx) {
|
|
|
297
283
|
return toolError(e);
|
|
298
284
|
}
|
|
299
285
|
}
|
|
300
|
-
|
|
301
286
|
function readPartner(args, caller) {
|
|
302
287
|
try {
|
|
303
288
|
var resolved = groupAndPartner(caller);
|
|
@@ -315,7 +300,6 @@ function attachSessionPair(ctx) {
|
|
|
315
300
|
return toolError(e);
|
|
316
301
|
}
|
|
317
302
|
}
|
|
318
|
-
|
|
319
303
|
function interruptPartner(args, caller) {
|
|
320
304
|
try {
|
|
321
305
|
var resolved = groupAndPartner(caller);
|
|
@@ -323,6 +307,7 @@ function attachSessionPair(ctx) {
|
|
|
323
307
|
if (!partner.isProcessing && !partner._queryStarting) {
|
|
324
308
|
return toolResult({ status: "idle", partnerId: partner.localId, title: partner.title || "New Session" });
|
|
325
309
|
}
|
|
310
|
+
taskControl.markInterruption(partner, "driver", args.reason || "The Driver interrupted the active task.");
|
|
326
311
|
partner.taskStopRequested = true;
|
|
327
312
|
if (partner.abortController) partner.abortController.abort();
|
|
328
313
|
return toolResult({ status: "interrupting", partnerId: partner.localId, title: partner.title || "New Session" });
|
|
@@ -330,17 +315,20 @@ function attachSessionPair(ctx) {
|
|
|
330
315
|
return toolError(e);
|
|
331
316
|
}
|
|
332
317
|
}
|
|
333
|
-
|
|
334
318
|
function closePartner(args, caller) {
|
|
335
319
|
try {
|
|
336
320
|
var resolved = groupAndPartner(caller);
|
|
337
321
|
var partner = resolved.partner;
|
|
338
322
|
var interrupted = !!(partner.isProcessing || partner._queryStarting);
|
|
339
323
|
if (interrupted) {
|
|
324
|
+
taskControl.markInterruption(partner, "driver", "The Driver closed the Split Worker pair.");
|
|
340
325
|
partner.taskStopRequested = true;
|
|
341
326
|
if (partner.abortController) partner.abortController.abort();
|
|
342
327
|
}
|
|
343
|
-
if (partner._pairDelegation)
|
|
328
|
+
if (partner._pairDelegation) {
|
|
329
|
+
completeInterruptedTask(partner, caller, partner._pairDelegation);
|
|
330
|
+
finishDelegation(resolved.group, caller, partner, partner._pairDelegation);
|
|
331
|
+
}
|
|
344
332
|
workerPermission.cancelForSession(partner, "The Driver closed the Split Worker pair.");
|
|
345
333
|
var ws = { _clayUser: caller.ownerId ? { id: caller.ownerId } : null };
|
|
346
334
|
var result = store.dissolve(ws, { id: resolved.group.id });
|
|
@@ -350,18 +338,24 @@ function attachSessionPair(ctx) {
|
|
|
350
338
|
return toolError(e);
|
|
351
339
|
}
|
|
352
340
|
}
|
|
353
|
-
|
|
354
341
|
function handleHumanStop(session) {
|
|
355
342
|
var roles = turnControl.markHumanStop(session);
|
|
356
343
|
if (!roles) return false;
|
|
344
|
+
taskControl.markInterruption(roles.worker, "user", "The human stopped this Split Worker turn.");
|
|
357
345
|
workerPermission.cancelForSession(roles.worker, "The human stopped this Split Worker turn.");
|
|
358
346
|
return true;
|
|
359
347
|
}
|
|
360
|
-
|
|
348
|
+
function beginHumanTurn(session) {
|
|
349
|
+
var resumed = turnControl.beginHumanTurn(session);
|
|
350
|
+
var roles = resumed && rolesFor(session);
|
|
351
|
+
if (roles) taskControl.drain(roles.driver, roles.worker);
|
|
352
|
+
return resumed;
|
|
353
|
+
}
|
|
361
354
|
function getToolDefs(boundSession) {
|
|
362
355
|
if (!boundSession) return pairMcp.getToolDefs({
|
|
363
356
|
send: function () { return toolError(new Error("send_to_partner requires a session-bound tool server")); },
|
|
364
357
|
read: function () { return toolError(new Error("read_partner requires a session-bound tool server")); },
|
|
358
|
+
message: function () { return toolError(new Error("message_partner requires a session-bound tool server")); },
|
|
365
359
|
interrupt: function () { return toolError(new Error("interrupt_partner requires a session-bound tool server")); },
|
|
366
360
|
close: function () { return toolError(new Error("close_partner requires a session-bound tool server")); },
|
|
367
361
|
status: function () { return toolError(new Error("partner_status requires a session-bound tool server")); },
|
|
@@ -369,7 +363,7 @@ function attachSessionPair(ctx) {
|
|
|
369
363
|
evaluate: function () { return toolError(new Error("record_partner_evaluation requires a session-bound tool server")); },
|
|
370
364
|
});
|
|
371
365
|
var group = store.groupForMember(boundSession.localId);
|
|
372
|
-
if (group && group.pair && group.pair.driverId !== boundSession.localId) return
|
|
366
|
+
if (group && group.pair && group.pair.driverId !== boundSession.localId) return taskControl.workerToolDefs(boundSession);
|
|
373
367
|
var adHocSplit = !!(group && !group.pair);
|
|
374
368
|
if (!driverEligibility.isEligibleDriverSession(boundSession, sm)) return [];
|
|
375
369
|
var persistentCatalog = boundSession.vendor === "codex" && (!group || !!group.pair);
|
|
@@ -382,34 +376,44 @@ function attachSessionPair(ctx) {
|
|
|
382
376
|
send: function (args) {
|
|
383
377
|
return turnControl.runOperation(boundSession, "send", args && args.operationId, function () {
|
|
384
378
|
return sendToPartner(args || {}, boundSession);
|
|
385
|
-
});
|
|
379
|
+
}, operationFingerprint(args, ["message", "wait", "timeoutSeconds", "taskId"])).catch(toolError);
|
|
386
380
|
},
|
|
387
381
|
read: function (args) { return readPartner(args, boundSession); },
|
|
382
|
+
message: function (args) {
|
|
383
|
+
return turnControl.runOperation(boundSession, "message", args && args.requestId, function () {
|
|
384
|
+
return pairMessage.send(args || {}, boundSession);
|
|
385
|
+
}, pairMessage.fingerprint(args || {})).catch(function (err) {
|
|
386
|
+
return toolResult({ status: "rejected", requestId: args && args.requestId || null, reason: err.message || String(err) });
|
|
387
|
+
});
|
|
388
|
+
},
|
|
388
389
|
interrupt: function (args) { return interruptPartner(args, boundSession); },
|
|
389
390
|
close: function (args) { return closePartner(args, boundSession); },
|
|
390
391
|
status: lifecycleHandlers.status,
|
|
391
392
|
replace: function (args) {
|
|
392
393
|
return turnControl.runOperation(boundSession, "replace", args && args.operationId, function () {
|
|
393
394
|
return workerProposal.proposeReplacement(args || {}, boundSession);
|
|
394
|
-
});
|
|
395
|
+
}, operationFingerprint(args, ["interrupt", "message", "workerVendor", "workerModel", "workerEffort", "recommendationRationale", "evaluation", "supersedeProposalId"])).catch(toolError);
|
|
395
396
|
},
|
|
396
397
|
evaluate: lifecycleHandlers.evaluate,
|
|
397
398
|
}, { lifecycle: persistentCatalog || !adHocSplit });
|
|
399
|
+
if (!adHocSplit) tools = tools.concat(taskControl.driverToolDefs({
|
|
400
|
+
queue: function (args) { return taskControl.queue(args || {}, boundSession); },
|
|
401
|
+
inspect: function (args) { return taskControl.inspect(args || {}, boundSession); },
|
|
402
|
+
cancel: function (args) { return taskControl.cancel(args || {}, boundSession); },
|
|
403
|
+
replace: function (args) { return taskControl.replaceTask(args || {}, boundSession); },
|
|
404
|
+
resume: function (args) { return taskControl.resumeTask(args || {}, boundSession); },
|
|
405
|
+
}));
|
|
398
406
|
if (persistentCatalog) {
|
|
399
407
|
return workerProposal.getToolDefs(boundSession, { persistent: true })
|
|
400
408
|
.concat(tools)
|
|
401
409
|
.concat(workerPermission.getToolDefs(boundSession, { dormantDriver: true }));
|
|
402
410
|
}
|
|
403
|
-
return tools
|
|
411
|
+
return tools.concat(adHocSplit ? [] : workerProposal.getToolDefs(boundSession, { persistent: true, controlsOnly: true }))
|
|
404
412
|
.concat(workerPermission.getToolDefs(boundSession, { dormantDriver: false }));
|
|
405
413
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
var createWorkerForDriver = factory.createWorkerForDriver;
|
|
410
|
-
var createPair = factory.createPair;
|
|
411
|
-
var preflightWorkerForDriver = factory.preflightWorkerForDriver;
|
|
412
|
-
|
|
414
|
+
var factory = attachPairFactory(ctx), createPairRecord = factory.createPairRecord,
|
|
415
|
+
createWorkerForDriver = factory.createWorkerForDriver, createPair = factory.createPair,
|
|
416
|
+
preflightWorkerForDriver = factory.preflightWorkerForDriver;
|
|
413
417
|
lifecycle = attachPairLifecycle({
|
|
414
418
|
sm: sm,
|
|
415
419
|
splitStore: store,
|
|
@@ -417,12 +421,13 @@ function attachSessionPair(ctx) {
|
|
|
417
421
|
preflightWorkerForDriver: function (driver, args) { return preflightWorkerForDriver(driver, args); },
|
|
418
422
|
sendToPartner: function (args, caller) { return sendToPartner(args, caller); },
|
|
419
423
|
finishDelegation: finishDelegation,
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
},
|
|
424
|
+
completeInterruptedTask: completeInterruptedTask,
|
|
425
|
+
cancelWorkerPermissions: function (worker, reason) { return workerPermission.cancelForSession(worker, reason); },
|
|
426
|
+
markInterruption: function (worker, by, reason) { return taskControl.markInterruption(worker, by, reason); },
|
|
427
|
+
taskStatus: function (worker) { return taskControl.status(worker); },
|
|
428
|
+
proposalStatus: function (driver) { return workerProposal ? workerProposal.proposalStatus(driver) : null; },
|
|
423
429
|
turnControl: turnControl,
|
|
424
430
|
});
|
|
425
|
-
|
|
426
431
|
workerPermission = attachWorkerPermission({
|
|
427
432
|
sm: sm,
|
|
428
433
|
splitStore: store,
|
|
@@ -430,7 +435,6 @@ function attachSessionPair(ctx) {
|
|
|
430
435
|
resumeDriverWithMessage: resumeDriverWithMessage,
|
|
431
436
|
requestDetach: requestDetach,
|
|
432
437
|
});
|
|
433
|
-
|
|
434
438
|
workerProposal = attachWorkerProposal({
|
|
435
439
|
sm: sm,
|
|
436
440
|
isMate: ctx.isMate,
|
|
@@ -446,8 +450,8 @@ function attachSessionPair(ctx) {
|
|
|
446
450
|
recordGenerationStart: function (driver, worker) { return lifecycle.recordGenerationStart(driver, worker); },
|
|
447
451
|
sendToPartner: sendToPartner,
|
|
448
452
|
replacePartner: function (args, session) { return lifecycle.replacePartner(args, session); },
|
|
453
|
+
preflightWorkerForDriver: preflightWorkerForDriver,
|
|
449
454
|
});
|
|
450
|
-
|
|
451
455
|
function handleMessage(ws, msg) {
|
|
452
456
|
if (workerProposal.handleMessage(ws, msg)) return true;
|
|
453
457
|
if (msg.type === "pair_session_options") {
|
|
@@ -467,8 +471,12 @@ function attachSessionPair(ctx) {
|
|
|
467
471
|
}
|
|
468
472
|
return false;
|
|
469
473
|
}
|
|
470
|
-
|
|
471
474
|
function getSystemPrompt(session) {
|
|
475
|
+
var roles = rolesFor(session);
|
|
476
|
+
if (roles && roles.worker === session) {
|
|
477
|
+
var taskId = session._pairDelegation && session._pairDelegation.taskId || "unknown";
|
|
478
|
+
return pairPrompts.worker(taskId);
|
|
479
|
+
}
|
|
472
480
|
var group = store.groupForMember(session.localId);
|
|
473
481
|
var pairPrompt = "";
|
|
474
482
|
if (group && group.pair && group.pair.driverId === session.localId) {
|
|
@@ -482,17 +490,8 @@ function attachSessionPair(ctx) {
|
|
|
482
490
|
}
|
|
483
491
|
return pairPrompt;
|
|
484
492
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
getToolDefs: getToolDefs,
|
|
489
|
-
handleHumanStop: handleHumanStop,
|
|
490
|
-
handleMessage: handleMessage,
|
|
491
|
-
handleTurnDone: handleTurnDone,
|
|
492
|
-
getSystemPrompt: getSystemPrompt,
|
|
493
|
-
respondToWorkerProposal: workerProposal.respondToProposal,
|
|
494
|
-
workerPermission: workerPermission,
|
|
495
|
-
};
|
|
493
|
+
return { beginHumanTurn: beginHumanTurn, getToolDefs: getToolDefs, handleHumanStop: handleHumanStop,
|
|
494
|
+
handleMessage: handleMessage, handleTurnDone: handleTurnDone, getSystemPrompt: getSystemPrompt,
|
|
495
|
+
respondToWorkerProposal: workerProposal.respondToProposal, workerPermission: workerPermission };
|
|
496
496
|
}
|
|
497
|
-
|
|
498
497
|
module.exports = { attachSessionPair: attachSessionPair, responseText: responseText, recentTurns: recentTurns };
|
package/lib/project-sessions.js
CHANGED
|
@@ -87,6 +87,13 @@ function attachSessions(ctx) {
|
|
|
87
87
|
var getLinuxUserForSession = ctx.getLinuxUserForSession;
|
|
88
88
|
var ensureProjectAccessForSession = ctx.ensureProjectAccessForSession;
|
|
89
89
|
var getOsUserInfoForWs = ctx.getOsUserInfoForWs;
|
|
90
|
+
function stopSession(session) {
|
|
91
|
+
if (!session || (!session.isProcessing && !session._queryStarting)) return false;
|
|
92
|
+
if (typeof ctx.onHumanPairStop === "function") ctx.onHumanPairStop(session);
|
|
93
|
+
session.taskStopRequested = true;
|
|
94
|
+
if (session.abortController) session.abortController.abort();
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
90
97
|
var hydrateImageRefs = ctx.hydrateImageRefs;
|
|
91
98
|
var onProcessingChanged = ctx.onProcessingChanged;
|
|
92
99
|
var broadcastPresence = ctx.broadcastPresence;
|
|
@@ -165,6 +172,14 @@ function attachSessions(ctx) {
|
|
|
165
172
|
var setLatestVersion = ctx.setLatestVersion;
|
|
166
173
|
var loadContextSources = ctx.loadContextSources;
|
|
167
174
|
|
|
175
|
+
function canAccessProjectSlug(ws, targetSlug) {
|
|
176
|
+
if (!targetSlug) return false;
|
|
177
|
+
if (!usersModule || typeof usersModule.isMultiUser !== "function" || !usersModule.isMultiUser()) return true;
|
|
178
|
+
var userId = ws && ws._clayUser ? ws._clayUser.id : null;
|
|
179
|
+
if (!userId || typeof opts.canAccessProjectSlug !== "function") return false;
|
|
180
|
+
return opts.canAccessProjectSlug(userId, targetSlug) === true;
|
|
181
|
+
}
|
|
182
|
+
|
|
168
183
|
// Resolve the active user's Claude open-mode preference ('gui' or 'tui').
|
|
169
184
|
// Multi-user mode reads per-user storage; single-user mode falls back to
|
|
170
185
|
// the daemon-level default ('gui').
|
|
@@ -1004,11 +1019,7 @@ function attachSessions(ctx) {
|
|
|
1004
1019
|
|
|
1005
1020
|
if (msg.type === "stop") {
|
|
1006
1021
|
var session = getSessionForWs(ws);
|
|
1007
|
-
|
|
1008
|
-
if (typeof ctx.onHumanPairStop === "function") ctx.onHumanPairStop(session);
|
|
1009
|
-
session.taskStopRequested = true;
|
|
1010
|
-
if (session.abortController) session.abortController.abort();
|
|
1011
|
-
}
|
|
1022
|
+
stopSession(session);
|
|
1012
1023
|
return true;
|
|
1013
1024
|
}
|
|
1014
1025
|
|
|
@@ -1770,6 +1781,10 @@ function attachSessions(ctx) {
|
|
|
1770
1781
|
|
|
1771
1782
|
// --- Create worktree from web UI ---
|
|
1772
1783
|
if (msg.type === "create_worktree") {
|
|
1784
|
+
if (slug.indexOf("--") !== -1) {
|
|
1785
|
+
sendTo(ws, { type: "create_worktree_result", ok: false, error: "Cannot create a worktree from another worktree" });
|
|
1786
|
+
return true;
|
|
1787
|
+
}
|
|
1773
1788
|
var wtBranch = (msg.branch || "").trim();
|
|
1774
1789
|
var wtDirName = (msg.dirName || "").trim() || wtBranch.replace(/\//g, "-");
|
|
1775
1790
|
var wtBase = (msg.baseBranch || "").trim() || null;
|
|
@@ -1793,7 +1808,7 @@ function attachSessions(ctx) {
|
|
|
1793
1808
|
// --- Pre-check: does the project have tasks/schedules? ---
|
|
1794
1809
|
if (msg.type === "remove_project_check") {
|
|
1795
1810
|
var checkSlug = msg.slug;
|
|
1796
|
-
if (!checkSlug) {
|
|
1811
|
+
if (!checkSlug || !canAccessProjectSlug(ws, checkSlug)) {
|
|
1797
1812
|
sendTo(ws, { type: "remove_project_check_result", slug: checkSlug, name: msg.name || checkSlug, count: 0 });
|
|
1798
1813
|
return true;
|
|
1799
1814
|
}
|
|
@@ -1816,8 +1831,16 @@ function attachSessions(ctx) {
|
|
|
1816
1831
|
sendTo(ws, { type: "remove_project_result", ok: false, error: "Missing slug" });
|
|
1817
1832
|
return true;
|
|
1818
1833
|
}
|
|
1834
|
+
if (!canAccessProjectSlug(ws, removeSlug)) {
|
|
1835
|
+
sendTo(ws, { type: "remove_project_result", ok: false, error: "You do not have access to this project" });
|
|
1836
|
+
return true;
|
|
1837
|
+
}
|
|
1819
1838
|
// If client chose to move tasks to another project before removing
|
|
1820
1839
|
if (msg.moveTasksTo) {
|
|
1840
|
+
if (!canAccessProjectSlug(ws, msg.moveTasksTo)) {
|
|
1841
|
+
sendTo(ws, { type: "remove_project_result", ok: false, error: "You do not have access to the destination project" });
|
|
1842
|
+
return true;
|
|
1843
|
+
}
|
|
1821
1844
|
moveAllSchedulesToProject(removeSlug, msg.moveTasksTo);
|
|
1822
1845
|
}
|
|
1823
1846
|
if (typeof opts.onRemoveProject === "function") {
|
|
@@ -1833,6 +1856,10 @@ function attachSessions(ctx) {
|
|
|
1833
1856
|
|
|
1834
1857
|
// --- Move a single schedule to another project ---
|
|
1835
1858
|
if (msg.type === "schedule_move") {
|
|
1859
|
+
if (!canAccessProjectSlug(ws, msg.fromSlug) || !canAccessProjectSlug(ws, msg.toSlug)) {
|
|
1860
|
+
sendTo(ws, { type: "schedule_move_result", ok: false, error: "You do not have access to one of these projects" });
|
|
1861
|
+
return true;
|
|
1862
|
+
}
|
|
1836
1863
|
var moveResult = moveScheduleToProject(msg.recordId, msg.fromSlug, msg.toSlug);
|
|
1837
1864
|
if (moveResult.ok) {
|
|
1838
1865
|
// Re-broadcast updated records to this project's clients
|
|
@@ -2071,6 +2098,13 @@ function attachSessions(ctx) {
|
|
|
2071
2098
|
handleSessionsMessage: handleSessionsMessage,
|
|
2072
2099
|
resolveSessionForView: resolveSessionForView,
|
|
2073
2100
|
respondToAskUser: respondToAskUser,
|
|
2101
|
+
respondToHomePermission: function (ws, msg, session) {
|
|
2102
|
+
if (!session || !msg || !session.pendingPermissions || !session.pendingPermissions[msg.requestId]) return false;
|
|
2103
|
+
var indexedSessionId = sm.permissionRequestIndex[msg.requestId];
|
|
2104
|
+
if (indexedSessionId !== session.localId || sm.sessions.get(indexedSessionId) !== session) return false;
|
|
2105
|
+
return handleSessionsMessage(ws, { type: "permission_response", requestId: msg.requestId, decision: msg.decision });
|
|
2106
|
+
},
|
|
2107
|
+
stopHomeSession: stopSession,
|
|
2074
2108
|
};
|
|
2075
2109
|
}
|
|
2076
2110
|
|