clay-server 4.3.0-beta.4 → 4.3.0-beta.6
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/driver-continuation-pair.js +5 -1
- package/lib/multi-worker-feature.js +11 -0
- package/lib/project-connection.js +2 -0
- package/lib/project-pair-autonomous-stop.js +14 -8
- package/lib/project-pair-close-transaction.js +59 -0
- package/lib/project-pair-global-stop.js +18 -0
- package/lib/project-pair-lifecycle-status.js +34 -0
- package/lib/project-pair-lifecycle.js +58 -76
- package/lib/project-pair-message.js +1 -1
- package/lib/project-pair-owned-stop.js +7 -4
- package/lib/project-pair-result-capture.js +116 -0
- package/lib/project-pair-result-delivery.js +140 -0
- package/lib/project-pair-result-outbox.js +400 -0
- package/lib/project-pair-result-pipeline.js +16 -0
- package/lib/project-pair-result-recovery.js +229 -0
- package/lib/project-pair-structural-close.js +20 -0
- package/lib/project-pair-task-control.js +45 -18
- package/lib/project-pair-worker-close.js +68 -0
- package/lib/project-session-pair-prompt.js +26 -0
- package/lib/project-session-pair.js +87 -106
- package/lib/project-worker-creation-proposal.js +176 -0
- package/lib/project-worker-permission.js +7 -8
- package/lib/project-worker-proposal-runner.js +45 -0
- package/lib/project-worker-proposal.js +79 -115
- package/lib/project.js +38 -1
- package/lib/public/app.js +1 -0
- package/lib/public/css/messages.css +12 -0
- package/lib/public/css/pane.css +48 -2
- package/lib/public/modules/app-connection.js +49 -76
- package/lib/public/modules/app-messages.js +11 -0
- package/lib/public/modules/pair-result-status.js +87 -0
- package/lib/public/modules/project-activation.js +10 -7
- package/lib/public/modules/sidebar-sessions.js +31 -11
- package/lib/public/modules/split-group-helpers.js +74 -2
- package/lib/public/modules/split-pair-ui.js +28 -6
- package/lib/public/modules/split-pane-reconciler.js +28 -0
- package/lib/public/modules/split-pane-renderer.js +107 -0
- package/lib/public/modules/split-view.js +58 -188
- package/lib/public/modules/websocket-watchdog.js +126 -0
- package/lib/public/modules/worker-pane-lock.js +13 -3
- package/lib/server.js +1 -0
- package/lib/session-pair-factory.js +66 -0
- package/lib/session-pair-mcp-server.js +10 -5
- package/lib/session-pair-prompts.js +41 -25
- package/lib/session-pair-target.js +42 -0
- package/lib/session-pair-turn-control.js +11 -6
- package/lib/session-split-group-anchors.js +89 -7
- package/lib/session-split-group-persistence.js +136 -0
- package/lib/session-split-group-roles.js +164 -0
- package/lib/session-split-group-v2-store.js +146 -0
- package/lib/session-split-groups.js +109 -99
- package/lib/sessions.js +33 -2
- package/lib/ws-schema.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
function attachProposalRunner(ctx) {
|
|
2
|
+
function resumeDriver(session, text) {
|
|
3
|
+
var sdk = ctx.getSdk();
|
|
4
|
+
if (!sdk) return Promise.reject(new Error("SDK bridge is not ready"));
|
|
5
|
+
ctx.sm.sendAndRecord(session, { type: "user_message", text: text, _internal: true });
|
|
6
|
+
session.sentToolResults = {};
|
|
7
|
+
if (!session.isProcessing) {
|
|
8
|
+
session.isProcessing = true;
|
|
9
|
+
ctx.onProcessingChanged();
|
|
10
|
+
ctx.sm.sendToSession(session, { type: "status", status: "processing" });
|
|
11
|
+
}
|
|
12
|
+
if (sdk.pushMessage(session, text)) return Promise.resolve();
|
|
13
|
+
return Promise.resolve(sdk.startQuery(session, text, undefined, ctx.getLinuxUserForSession(session)));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseResult(result) {
|
|
17
|
+
if (!result || result.isError || !result.content || !result.content[0]) {
|
|
18
|
+
return { status: "error", error: "Split Worker execution failed." };
|
|
19
|
+
}
|
|
20
|
+
try { return JSON.parse(result.content[0].text); }
|
|
21
|
+
catch (error) { return { status: "error", error: result.content[0].text || "Split Worker execution failed." }; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function runWorker(session, proposal) {
|
|
25
|
+
var result = parseResult(await ctx.sendToPartner({ message: proposal.message, wait: true,
|
|
26
|
+
timeoutSeconds: 900, workerId: proposal.workerId }, session));
|
|
27
|
+
ctx.updateProposal(session, proposal, { status: result.status === "complete" ? "completed" : result.status || "error",
|
|
28
|
+
resultPreview: result.response ? result.response.slice(0, 1200) : "", error: result.error || null });
|
|
29
|
+
var followup;
|
|
30
|
+
if (result.status === "complete") {
|
|
31
|
+
followup = "[Split Worker execution completed]\nReview and verify the Split Worker's result. The Split Worker session remains available: if the implementation needs corrections or additional edits, send a follow-up with send_to_partner instead of taking over the Split Worker-owned files yourself. If that Split Worker is no longer available, keep the work in the visible Split Worker flow and use send_to_partner again after the stale pair is removed; never substitute a background Sub-agent.\n\n" + (result.response || "The Split Worker completed without a text summary.");
|
|
32
|
+
} else if (result.status === "interrupted") {
|
|
33
|
+
followup = "[Split Worker execution interrupted]\nThe Split Worker stopped mid-turn. Its work is PARTIAL and unverified — do not treat it as finished. Check partner_status before deciding next steps; this result alone does not identify who interrupted it. If the human stopped it, do not retry until a new human Driver message. Otherwise continue within the user's authorized task.";
|
|
34
|
+
} else if (result.status === "running") {
|
|
35
|
+
followup = "[Split Worker execution is still running]\nUse read_partner to inspect progress before completing the task.";
|
|
36
|
+
} else {
|
|
37
|
+
followup = "[Split Worker execution failed]\nThe existing Split Worker may still be reusable. Inspect partner_status and send a narrower follow-up with send_to_partner when the Worker and pair still exist; do not replace it merely because its task or transport failed. Retry replacement only when the replacement transaction reports that no new Worker was created and the failure is explicitly safe to retry.\n\n" + (result.error || result.response || "Unknown Split Worker error.");
|
|
38
|
+
}
|
|
39
|
+
await resumeDriver(session, followup);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { resumeDriver: resumeDriver, runWorker: runWorker };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { attachProposalRunner: attachProposalRunner };
|
|
@@ -3,8 +3,12 @@ var yoke = require("./yoke");
|
|
|
3
3
|
var buildShape = require("./session-spawn-mcp-server").buildShape;
|
|
4
4
|
var driverEligibility = require("./session-driver-eligibility");
|
|
5
5
|
var driverOrchestration = require("./session-driver-orchestration");
|
|
6
|
+
var pairTarget = require("./session-pair-target");
|
|
7
|
+
var splitRoles = require("./session-split-group-roles");
|
|
6
8
|
var proposalControl = require("./worker-proposal-control");
|
|
7
9
|
var runtimeCatalog = require("./worker-runtime-catalog");
|
|
10
|
+
var proposalRunner = require("./project-worker-proposal-runner");
|
|
11
|
+
var creationProposalModule = require("./project-worker-creation-proposal");
|
|
8
12
|
var MAX_SUMMARY_CHARS = 600, MAX_PLAN_CHARS = 6000, MAX_TASK_CHARS = 30000, MAX_RATIONALE_CHARS = 1000;
|
|
9
13
|
var EVALUATION_OUTCOMES = ["succeeded", "partial", "failed", "abandoned"];
|
|
10
14
|
function normalizeEvaluation(raw) {
|
|
@@ -161,66 +165,18 @@ function attachWorkerProposal(ctx) {
|
|
|
161
165
|
proposalId: proposal.proposalId,
|
|
162
166
|
}, patch));
|
|
163
167
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return toolResult({ error: "summary, plan, message, and recommendationRationale are required." });
|
|
177
|
-
}
|
|
178
|
-
if (task.length > MAX_TASK_CHARS) return toolResult({ error: "The Split Worker task is too long." });
|
|
179
|
-
await ensureModelCatalogs();
|
|
180
|
-
if (!isEligible(session)) {
|
|
181
|
-
return toolResult({ error: "The Driver session changed while Split Worker runtimes were loading." });
|
|
182
|
-
}
|
|
183
|
-
var currentPending = proposalControl.pendingProposal(session);
|
|
184
|
-
if (currentPending !== superseded) return toolResult({ error: "The pending Split Worker suggestion changed while runtimes were loading." });
|
|
185
|
-
var options = proposalOptions();
|
|
186
|
-
if (options.installedVendors.length === 0) return toolResult({ error: "No coding agent is installed for a Split Worker session." });
|
|
187
|
-
var recommendation = chooseRecommendation(args, session, options);
|
|
188
|
-
var proposal = {
|
|
189
|
-
type: "worker_proposal",
|
|
190
|
-
proposalId: "worker_" + crypto.randomUUID(),
|
|
191
|
-
summary: summary.slice(0, MAX_SUMMARY_CHARS),
|
|
192
|
-
plan: plan.slice(0, MAX_PLAN_CHARS),
|
|
193
|
-
message: task,
|
|
194
|
-
status: "pending",
|
|
195
|
-
createdAt: Date.now(),
|
|
196
|
-
recommendedVendor: recommendation.vendor,
|
|
197
|
-
recommendedModel: recommendation.model,
|
|
198
|
-
recommendedEffort: recommendation.effort,
|
|
199
|
-
recommendationRationale: rationale.slice(0, MAX_RATIONALE_CHARS),
|
|
200
|
-
options: options,
|
|
201
|
-
};
|
|
202
|
-
if (superseded) updateProposal(session, superseded, { status: "superseded", supersededBy: proposal.proposalId });
|
|
203
|
-
sm.sendAndRecord(session, proposal);
|
|
204
|
-
if (skipPermissionsEnabled(session) && recommendationCanAutoAccept(args, recommendation, options)) {
|
|
205
|
-
var accepted = await acceptProposal(session, proposal, {
|
|
206
|
-
vendor: recommendation.vendor,
|
|
207
|
-
model: recommendation.model,
|
|
208
|
-
effort: recommendation.effort,
|
|
209
|
-
}, autoAcceptanceWs(session), true);
|
|
210
|
-
return toolResult({
|
|
211
|
-
status: accepted.ok ? "auto_accepted" : "posted",
|
|
212
|
-
proposalId: proposal.proposalId,
|
|
213
|
-
instruction: accepted.ok
|
|
214
|
-
? "The recorded Split Worker configuration was auto-accepted under the Driver session's full-access mode. End this turn now while the exact paired Worker runs."
|
|
215
|
-
: "The automatic decision failed closed. The configuration card remains pending for the user; end this turn and wait for their decision.",
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
return toolResult({
|
|
219
|
-
status: "posted",
|
|
220
|
-
proposalId: proposal.proposalId,
|
|
221
|
-
instruction: "The Split Worker suggestion is visible in the chat. End this turn now and wait for the user's decision.",
|
|
222
|
-
});
|
|
223
|
-
}
|
|
168
|
+
var runner = proposalRunner.attachProposalRunner(Object.assign({}, ctx, { updateProposal: updateProposal }));
|
|
169
|
+
var resumeDriver = runner.resumeDriver;
|
|
170
|
+
var runWorker = runner.runWorker;
|
|
171
|
+
var creationProposal = creationProposalModule.attachWorkerCreationProposal({
|
|
172
|
+
sm: sm, splitStore: store, multiWorkerFeature: ctx.multiWorkerFeature,
|
|
173
|
+
canOffer: canOffer, isEligible: isEligible, ensureModelCatalogs: ensureModelCatalogs,
|
|
174
|
+
proposalOptions: proposalOptions, chooseRecommendation: chooseRecommendation,
|
|
175
|
+
skipPermissionsEnabled: skipPermissionsEnabled, recommendationCanAutoAccept: recommendationCanAutoAccept,
|
|
176
|
+
autoAcceptanceWs: autoAcceptanceWs, acceptProposal: acceptProposal, updateProposal: updateProposal,
|
|
177
|
+
toolResult: toolResult, maxSummaryChars: MAX_SUMMARY_CHARS, maxPlanChars: MAX_PLAN_CHARS,
|
|
178
|
+
maxTaskChars: MAX_TASK_CHARS, maxRationaleChars: MAX_RATIONALE_CHARS,
|
|
179
|
+
});
|
|
224
180
|
async function proposeReplacement(args, session) {
|
|
225
181
|
var group = session && store.groupForMember(session.localId);
|
|
226
182
|
if (ctx.isMate || !isLiveSession(session) || !driverEligibility.isEligibleDriverSession(session, sm) || !group ||
|
|
@@ -228,7 +184,17 @@ function attachWorkerProposal(ctx) {
|
|
|
228
184
|
return toolResult({ error: "Split Worker replacement proposals require the exact paired Driver session." });
|
|
229
185
|
}
|
|
230
186
|
var sourceGroupId = group.id;
|
|
231
|
-
var
|
|
187
|
+
var addressed;
|
|
188
|
+
try { addressed = pairTarget.resolveGroupTarget(sm, group, session, args, {
|
|
189
|
+
mutation: true, multiWorkerFeature: ctx.multiWorkerFeature,
|
|
190
|
+
}); }
|
|
191
|
+
catch (e) { return toolResult({ error: e.message || String(e) }); }
|
|
192
|
+
var sourceWorkerId = addressed.workerId;
|
|
193
|
+
var sourceWorkerOriginId = addressed.partner.sessionOriginId || null;
|
|
194
|
+
var sourceGeneration = addressed.partner._pairGeneration ||
|
|
195
|
+
addressed.partner.sessionProvenance && addressed.partner.sessionProvenance.generation || null;
|
|
196
|
+
var sourceMembership = creationProposalModule.captureMembership(sm, session, group);
|
|
197
|
+
if (!sourceMembership) return toolResult({ error: "The exact Driver/Split Worker identities are not durable enough to replace a Worker." });
|
|
232
198
|
var superseded = proposalControl.pendingProposal(session);
|
|
233
199
|
var supersedeId = typeof args.supersedeProposalId === "string" ? args.supersedeProposalId.trim() : "";
|
|
234
200
|
if (superseded && superseded.proposalId !== supersedeId) return toolResult({ error: "A Split Worker proposal is already awaiting a decision. Inspect it or supply its exact id to supersede it." });
|
|
@@ -245,9 +211,17 @@ function attachWorkerProposal(ctx) {
|
|
|
245
211
|
var liveGroup = store.groupForMember(session.localId);
|
|
246
212
|
if (!isLiveSession(session) || !driverEligibility.isEligibleDriverSession(session, sm) ||
|
|
247
213
|
!liveGroup || liveGroup.id !== sourceGroupId || !liveGroup.pair ||
|
|
248
|
-
liveGroup.pair.driverId !== session.localId || liveGroup.
|
|
214
|
+
liveGroup.pair.driverId !== session.localId || liveGroup.members.indexOf(sourceWorkerId) === -1 ||
|
|
215
|
+
sm.sessions.get(sourceWorkerId) !== addressed.partner ||
|
|
216
|
+
(addressed.partner.sessionOriginId || null) !== sourceWorkerOriginId ||
|
|
217
|
+
(addressed.partner._pairGeneration || addressed.partner.sessionProvenance && addressed.partner.sessionProvenance.generation || null) !== sourceGeneration ||
|
|
218
|
+
!creationProposalModule.matchesMembership(sm, session, liveGroup, sourceMembership)) {
|
|
249
219
|
return toolResult({ error: "The Driver/Split Worker pair changed while replacement runtimes were loading." });
|
|
250
220
|
}
|
|
221
|
+
try { pairTarget.resolveGroupTarget(sm, liveGroup, session, args, {
|
|
222
|
+
mutation: true, multiWorkerFeature: ctx.multiWorkerFeature,
|
|
223
|
+
}); }
|
|
224
|
+
catch (e) { return toolResult({ error: e.message || String(e) }); }
|
|
251
225
|
var currentPending = proposalControl.pendingProposal(session);
|
|
252
226
|
if (currentPending !== superseded) return toolResult({ error: "The pending Split Worker proposal changed while runtimes were loading." });
|
|
253
227
|
var options = proposalOptions();
|
|
@@ -274,6 +248,10 @@ function attachWorkerProposal(ctx) {
|
|
|
274
248
|
options: options,
|
|
275
249
|
sourceGroupId: sourceGroupId,
|
|
276
250
|
sourceWorkerId: sourceWorkerId,
|
|
251
|
+
sourceWorkerOriginId: sourceWorkerOriginId,
|
|
252
|
+
sourceGeneration: sourceGeneration,
|
|
253
|
+
sourceMembership: sourceMembership,
|
|
254
|
+
workerId: sourceWorkerId,
|
|
277
255
|
interrupt: args.interrupt === true,
|
|
278
256
|
evaluation: normalizedEvaluation ? normalizedEvaluation.value : null,
|
|
279
257
|
transactionId: "replace_" + crypto.randomUUID(),
|
|
@@ -300,50 +278,6 @@ function attachWorkerProposal(ctx) {
|
|
|
300
278
|
instruction: "The replacement configuration is visible in the chat. End this turn now and wait for the user's decision.",
|
|
301
279
|
});
|
|
302
280
|
}
|
|
303
|
-
function resumeDriver(session, text) {
|
|
304
|
-
var sdk = ctx.getSdk();
|
|
305
|
-
if (!sdk) return Promise.reject(new Error("SDK bridge is not ready"));
|
|
306
|
-
var message = { type: "user_message", text: text, _internal: true };
|
|
307
|
-
sm.sendAndRecord(session, message);
|
|
308
|
-
session.sentToolResults = {};
|
|
309
|
-
if (!session.isProcessing) {
|
|
310
|
-
session.isProcessing = true;
|
|
311
|
-
ctx.onProcessingChanged();
|
|
312
|
-
sm.sendToSession(session, { type: "status", status: "processing" });
|
|
313
|
-
}
|
|
314
|
-
if (sdk.pushMessage(session, text)) return Promise.resolve();
|
|
315
|
-
return Promise.resolve(sdk.startQuery(session, text, undefined, ctx.getLinuxUserForSession(session)));
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function parsePartnerResult(result) {
|
|
319
|
-
if (!result || result.isError || !result.content || !result.content[0]) {
|
|
320
|
-
return { status: "error", error: "Split Worker execution failed." };
|
|
321
|
-
}
|
|
322
|
-
try { return JSON.parse(result.content[0].text); }
|
|
323
|
-
catch (e) { return { status: "error", error: result.content[0].text || "Split Worker execution failed." }; }
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
async function runWorker(session, proposal) {
|
|
327
|
-
var result = parsePartnerResult(await ctx.sendToPartner({ message: proposal.message, wait: true, timeoutSeconds: 900 }, session));
|
|
328
|
-
var status = result.status === "complete" ? "completed" : result.status;
|
|
329
|
-
updateProposal(session, proposal, {
|
|
330
|
-
status: status || "error",
|
|
331
|
-
resultPreview: result.response ? result.response.slice(0, 1200) : "",
|
|
332
|
-
error: result.error || null,
|
|
333
|
-
});
|
|
334
|
-
var followup;
|
|
335
|
-
if (result.status === "complete") {
|
|
336
|
-
followup = "[Split Worker execution completed]\nReview and verify the Split Worker's result. The Split Worker session remains available: if the implementation needs corrections or additional edits, send a follow-up with send_to_partner instead of taking over the Split Worker-owned files yourself. If that Split Worker is no longer available, keep the work in the visible Split Worker flow and use send_to_partner again after the stale pair is removed; never substitute a background Sub-agent.\n\n" + (result.response || "The Split Worker completed without a text summary.");
|
|
337
|
-
} else if (result.status === "interrupted") {
|
|
338
|
-
followup = "[Split Worker execution interrupted]\nThe Split Worker stopped mid-turn. Its work is PARTIAL and unverified — do not treat it as finished. Check partner_status before deciding next steps; this result alone does not identify who interrupted it. If the human stopped it, do not retry until a new human Driver message. Otherwise continue within the user's authorized task.";
|
|
339
|
-
} else if (result.status === "running") {
|
|
340
|
-
followup = "[Split Worker execution is still running]\nUse read_partner to inspect progress before completing the task.";
|
|
341
|
-
} else {
|
|
342
|
-
followup = "[Split Worker execution failed]\nThe existing Split Worker may still be reusable. Inspect partner_status and send a narrower follow-up with send_to_partner when the Worker and pair still exist; do not replace it merely because its task or transport failed. Retry replacement only when the replacement transaction reports that no new Worker was created and the failure is explicitly safe to retry.\n\n" + (result.error || result.response || "Unknown Split Worker error.");
|
|
343
|
-
}
|
|
344
|
-
await resumeDriver(session, followup);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
281
|
function sessionForResponse(ws) {
|
|
348
282
|
var session = sm.sessions.get(ws._clayActiveSession);
|
|
349
283
|
if (!session) throw new Error("active Driver session not found");
|
|
@@ -356,19 +290,28 @@ function attachWorkerProposal(ctx) {
|
|
|
356
290
|
|
|
357
291
|
async function acceptProposal(session, proposal, msg, ws, trustedAutoAcceptance) {
|
|
358
292
|
if (!isLiveSession(session)) throw new Error("The Driver session is no longer live");
|
|
359
|
-
if (proposal.action
|
|
293
|
+
if (!proposal.action && !isEligible(session)) {
|
|
360
294
|
throw new Error("The Driver is no longer eligible to create this Split Worker");
|
|
361
295
|
}
|
|
362
296
|
var approvedGroup = null;
|
|
363
|
-
if (proposal.action === "
|
|
297
|
+
if (proposal.action === "add") {
|
|
298
|
+
var addition = creationProposal.validateAcceptance(session, proposal);
|
|
299
|
+
if (!addition.ok) throw new Error("The Driver/Split Worker membership changed before the addition was approved");
|
|
300
|
+
approvedGroup = addition.group;
|
|
301
|
+
} else if (proposal.action === "replace") {
|
|
364
302
|
approvedGroup = store.groupForMember(session.localId);
|
|
303
|
+
var approvedRoles = splitRoles.normalizePair(approvedGroup && approvedGroup.pair, approvedGroup && approvedGroup.members);
|
|
304
|
+
var approvedWorker = sm.sessions.get(proposal.sourceWorkerId);
|
|
365
305
|
if (!driverEligibility.isEligibleDriverSession(session, sm) || !approvedGroup ||
|
|
366
|
-
approvedGroup.id !== proposal.sourceGroupId || !
|
|
367
|
-
|
|
306
|
+
approvedGroup.id !== proposal.sourceGroupId || !approvedRoles.ok || approvedRoles.kind === "adhoc" ||
|
|
307
|
+
approvedRoles.driverId !== session.localId || approvedRoles.workerIds.indexOf(proposal.sourceWorkerId) === -1 ||
|
|
308
|
+
!approvedWorker || (approvedWorker.sessionOriginId || null) !== (proposal.sourceWorkerOriginId || null) ||
|
|
309
|
+
(approvedWorker._pairGeneration || approvedWorker.sessionProvenance && approvedWorker.sessionProvenance.generation || null) !== proposal.sourceGeneration ||
|
|
310
|
+
!creationProposalModule.matchesMembership(sm, session, approvedGroup, proposal.sourceMembership)) {
|
|
368
311
|
throw new Error("The Driver/Split Worker pair changed before the replacement was approved");
|
|
369
312
|
}
|
|
370
313
|
}
|
|
371
|
-
var options = proposalOptions();
|
|
314
|
+
var options = proposal.options || proposalOptions();
|
|
372
315
|
var vendor = msg.vendor || proposal.recommendedVendor;
|
|
373
316
|
var model = msg.model || "";
|
|
374
317
|
if (options.installedVendors.indexOf(vendor) === -1) throw new Error("Selected Split Worker vendor is not installed");
|
|
@@ -390,6 +333,19 @@ function attachWorkerProposal(ctx) {
|
|
|
390
333
|
};
|
|
391
334
|
updateProposal(session, proposal, startingPatch);
|
|
392
335
|
try {
|
|
336
|
+
if (proposal.action === "add") {
|
|
337
|
+
var added = ctx.addWorkerForDriver(session, {
|
|
338
|
+
workerVendor: vendor, workerModel: model, workerEffort: effort,
|
|
339
|
+
expectedGroup: approvedGroup, expectedWorkerIds: proposal.sourceWorkerIds,
|
|
340
|
+
});
|
|
341
|
+
ctx.recordGenerationStart(session, added.worker);
|
|
342
|
+
updateProposal(session, proposal, { status: "running", groupId: added.group.id, workerId: added.worker.localId });
|
|
343
|
+
runWorker(session, proposal).catch(function (err) {
|
|
344
|
+
updateProposal(session, proposal, { status: "error", error: err.message || String(err) });
|
|
345
|
+
resumeDriver(session, "[Split Worker execution failed]\n" + (err.message || String(err))).catch(function () {});
|
|
346
|
+
});
|
|
347
|
+
return { ok: true, status: "running", group: added.group, addition: added };
|
|
348
|
+
}
|
|
393
349
|
if (proposal.action === "replace") {
|
|
394
350
|
var liveGroup = approvedGroup;
|
|
395
351
|
var replaced = await ctx.replacePartner({
|
|
@@ -399,6 +355,7 @@ function attachWorkerProposal(ctx) {
|
|
|
399
355
|
workerEffort: effort,
|
|
400
356
|
evaluation: proposal.evaluation || undefined,
|
|
401
357
|
transactionId: proposal.transactionId,
|
|
358
|
+
workerId: proposal.workerId,
|
|
402
359
|
}, session);
|
|
403
360
|
liveGroup = store.groupForMember(session.localId);
|
|
404
361
|
updateProposal(session, proposal, { status: "running", groupId: liveGroup.id, workerId: replaced.workerSessionId });
|
|
@@ -423,9 +380,11 @@ function attachWorkerProposal(ctx) {
|
|
|
423
380
|
} catch (err) {
|
|
424
381
|
if (proposal.action === "replace") {
|
|
425
382
|
var restoredGroup = store.groupForMember(session.localId);
|
|
426
|
-
|
|
427
|
-
|
|
383
|
+
var restoredRoles = splitRoles.normalizePair(restoredGroup && restoredGroup.pair, restoredGroup && restoredGroup.members);
|
|
384
|
+
if (restoredGroup && restoredRoles.ok && restoredRoles.kind !== "adhoc" &&
|
|
385
|
+
restoredRoles.driverId === session.localId && restoredRoles.workerIds.indexOf(proposal.sourceWorkerId) !== -1) {
|
|
428
386
|
proposal.sourceGroupId = restoredGroup.id;
|
|
387
|
+
proposal.sourceMembership = creationProposalModule.captureMembership(sm, session, restoredGroup);
|
|
429
388
|
}
|
|
430
389
|
proposal.transactionId = "replace_" + crypto.randomUUID();
|
|
431
390
|
}
|
|
@@ -449,6 +408,8 @@ function attachWorkerProposal(ctx) {
|
|
|
449
408
|
updateProposal(session, proposal, { status: "declined" });
|
|
450
409
|
var declinedText = proposal.action === "replace"
|
|
451
410
|
? "[Split Worker replacement declined]\nContinue with the existing Split Worker and the current task."
|
|
411
|
+
: proposal.action === "add"
|
|
412
|
+
? "[Additional Split Worker declined]\nContinue with the existing Split Worker and current plan."
|
|
452
413
|
: "[Split Worker proposal declined]\nContinue this task in the current Driver session using the plan you already prepared.";
|
|
453
414
|
await resumeDriver(session, declinedText);
|
|
454
415
|
return { ok: true, status: "declined" };
|
|
@@ -468,7 +429,7 @@ function attachWorkerProposal(ctx) {
|
|
|
468
429
|
var persistent = !!(options && options.persistent);
|
|
469
430
|
var controlsOnly = !!(options && options.controlsOnly);
|
|
470
431
|
if (!isEligible(session) && !(persistent && canOffer(session))) return [];
|
|
471
|
-
var proposalTools = controlsOnly ? [] : [{
|
|
432
|
+
var proposalTools = controlsOnly && !creationProposal.canMountProposal(session) ? [] : [{
|
|
472
433
|
name: "propose_worker",
|
|
473
434
|
description: "Propose a visible Split Worker for implementation-heavy execution. This non-mutating tool only shows the user a card where they choose vendor, model, and effort. It never creates a session or delegates work until the user accepts.",
|
|
474
435
|
inputSchema: buildShape({
|
|
@@ -481,7 +442,7 @@ function attachWorkerProposal(ctx) {
|
|
|
481
442
|
recommendationRationale: { type: "string", description: "Concise Driver-authored explanation of why the recommended vendor, model, and effort fit this exact task." },
|
|
482
443
|
supersedeProposalId: { type: "string", description: "Exact pending proposal id to resolve as superseded when posting this replacement suggestion." },
|
|
483
444
|
}, ["summary", "plan", "message", "recommendationRationale"]),
|
|
484
|
-
handler: function (args) { return propose(args || {}, session); },
|
|
445
|
+
handler: function (args) { return creationProposal.propose(args || {}, session); },
|
|
485
446
|
}];
|
|
486
447
|
return proposalTools.concat(proposalControl.getToolDefs({
|
|
487
448
|
inspect: function () { return toolResult(isLiveSession(session) ? proposalControl.projection(proposalControl.pendingProposal(session)) : { status: "rejected", reason: "The Driver session is no longer live." }); },
|
|
@@ -501,6 +462,9 @@ function attachWorkerProposal(ctx) {
|
|
|
501
462
|
}
|
|
502
463
|
|
|
503
464
|
function getSystemPrompt(session) {
|
|
465
|
+
if (creationProposal.canMountProposal(session) && !isEligible(session)) {
|
|
466
|
+
return "This Driver may configure one additional Split Worker through propose_worker. With two Workers, pass workerId to every targeted partner, queue, interrupt, replacement, close, permission, and status operation. Give each Worker independent file ownership and bounded work. Do not assign simultaneous overlapping file edits unless the work is explicitly separated, such as by distinct paths or worktrees.";
|
|
467
|
+
}
|
|
504
468
|
if (!isEligible(session) || !driverOrchestration.isHighTierDriverSession(session, sm)) return "";
|
|
505
469
|
return "For implementation-heavy work, use propose_worker before substantial execution. Work is implementation-heavy when it spans multiple modules, crosses client/server/data boundaries, requires a migration, or contains independent investigation and execution. Do not skip delegation merely because you can implement it yourself; preserve the Driver for decomposition, runtime selection, judgment, review, and integration. The user's explicit request to work directly takes precedence, and work that must remain sequential in this session stays here. Recommend an exact available vendor, model, and effort, and give a concise recommendationRationale explaining why all three fit the task. Clay always records and shows the runtime configuration card. In full-access mode Clay may auto-accept that exact validated recommendation; otherwise it waits for the user's explicit choice. After posting, end the turn. If accepted, Clay creates the exact Driver/Split Worker pair and delegates the proposed task once. If declined, continue in this Driver session. Do not call send_to_partner while unpaired.";
|
|
506
470
|
}
|
package/lib/project.js
CHANGED
|
@@ -55,6 +55,8 @@ var { attachDelegatedFollowUp } = require("./project-delegated-follow-up");
|
|
|
55
55
|
var { attachCapsuleTurn } = require("./project-capsule-turn");
|
|
56
56
|
var workspaceSessionRef = require("./workspace-query-service").sessionRef;
|
|
57
57
|
var { attachSessionPair } = require("./project-session-pair");
|
|
58
|
+
var { attachPairResultOutbox } = require("./project-pair-result-outbox");
|
|
59
|
+
var { attachPairResultRecovery } = require("./project-pair-result-recovery");
|
|
58
60
|
var { attachSessionHandoff } = require("./project-session-handoff");
|
|
59
61
|
var { attachDriverContinuation } = require("./project-driver-continuation");
|
|
60
62
|
var driverContinuationAccess = require("./driver-continuation-access");
|
|
@@ -66,6 +68,7 @@ var { attachProjectLogs } = require("./project-logs");
|
|
|
66
68
|
var { attachProjectLogFeedbackDelivery } = require("./project-log-feedback-delivery");
|
|
67
69
|
var { attachProjectMateKnowledge } = require("./project-mate-knowledge");
|
|
68
70
|
var { attachSplitGroups } = require("./session-split-groups");
|
|
71
|
+
var multiWorkerFeature = require("./multi-worker-feature");
|
|
69
72
|
var toolControlMcp = require("./tool-control-mcp-server");
|
|
70
73
|
var capsuleFloor = require("./capsule-display-floor");
|
|
71
74
|
var toolLlm = require("./tool-llm");
|
|
@@ -205,6 +208,7 @@ function safeAbsPath(requested) {
|
|
|
205
208
|
* opts: { cwd, slug, title, pushModule, debug, dangerouslySkipPermissions, currentVersion }
|
|
206
209
|
*/
|
|
207
210
|
function createProjectContext(opts) {
|
|
211
|
+
var multiWorkerGate = multiWorkerFeature.fromServerConfig(opts);
|
|
208
212
|
var cwd = opts.cwd;
|
|
209
213
|
var slug = opts.slug;
|
|
210
214
|
var project = path.basename(cwd);
|
|
@@ -659,6 +663,7 @@ function createProjectContext(opts) {
|
|
|
659
663
|
clients: clients,
|
|
660
664
|
sendTo: sendTo,
|
|
661
665
|
usersModule: usersModule,
|
|
666
|
+
multiWorkerFeature: multiWorkerGate,
|
|
662
667
|
onPairChanged: function (group) {
|
|
663
668
|
if (!sdk || !group || !Array.isArray(group.members)) return;
|
|
664
669
|
for (var i = 0; i < group.members.length; i++) {
|
|
@@ -819,10 +824,14 @@ function createProjectContext(opts) {
|
|
|
819
824
|
|
|
820
825
|
// The SDK bridge is created after local MCP servers. Session spawning uses
|
|
821
826
|
// a getter so tool handlers see the initialized bridge when they run.
|
|
827
|
+
var _pairResultOutbox = attachPairResultOutbox({ storageDir: sm.sessionsDir, projectSlug: slug });
|
|
828
|
+
_pairResultOutbox.recoverPending(sm.sessions);
|
|
829
|
+
var _pairResultRecovery = null;
|
|
822
830
|
var _sessionPair = attachSessionPair({
|
|
823
831
|
sm: sm,
|
|
824
832
|
isMate: isMate,
|
|
825
833
|
splitStore: _splitGroups.store,
|
|
834
|
+
multiWorkerFeature: multiWorkerGate,
|
|
826
835
|
getSdk: function () { return sdk; },
|
|
827
836
|
send: send,
|
|
828
837
|
sendTo: sendTo,
|
|
@@ -837,12 +846,31 @@ function createProjectContext(opts) {
|
|
|
837
846
|
usersModule: usersModule,
|
|
838
847
|
getLinuxUserForSession: getLinuxUserForSession,
|
|
839
848
|
onProcessingChanged: onProcessingChanged,
|
|
849
|
+
projectSlug: slug,
|
|
850
|
+
resultOutbox: _pairResultOutbox,
|
|
851
|
+
onResultStateChange: function (driver, record) { if (_pairResultRecovery) _pairResultRecovery.stateChanged(driver, record); },
|
|
840
852
|
adapters: adapters,
|
|
841
853
|
onPartnerResult: function (driver, worker, token) {
|
|
842
854
|
if (_scheduledTaskExecution) _scheduledTaskExecution.onPartnerResult(driver, worker, token);
|
|
843
855
|
if (_autonomousRun) _autonomousRun.onPartnerResult(driver, worker, token);
|
|
844
856
|
},
|
|
845
857
|
});
|
|
858
|
+
var _pairWorkerClose = require("./project-pair-worker-close").attachPairWorkerClose({
|
|
859
|
+
sm: sm,
|
|
860
|
+
splitStore: _splitGroups.store,
|
|
861
|
+
multiWorkerFeature: multiWorkerGate,
|
|
862
|
+
isMate: isMate,
|
|
863
|
+
projectSlug: slug,
|
|
864
|
+
hasSocket: function (ws) { return clients.has(ws); },
|
|
865
|
+
sendTo: sendTo,
|
|
866
|
+
closeWorker: _sessionPair.closeWorker,
|
|
867
|
+
});
|
|
868
|
+
_pairResultRecovery = attachPairResultRecovery({
|
|
869
|
+
sm: sm, store: _splitGroups.store, outbox: _pairResultOutbox, resultCapture: _sessionPair.resultCapture,
|
|
870
|
+
wake: _sessionPair.wakePartnerResult, projectSlug: slug, sendTo: sendTo,
|
|
871
|
+
blockedReason: _sessionPair.stopBarrier,
|
|
872
|
+
getClients: function () { return clients; }, isMultiUser: usersModule.isMultiUser,
|
|
873
|
+
});
|
|
846
874
|
var _sessionSpawn = attachSessionSpawn({
|
|
847
875
|
cwd: cwd,
|
|
848
876
|
sm: sm,
|
|
@@ -1277,6 +1305,7 @@ function createProjectContext(opts) {
|
|
|
1277
1305
|
},
|
|
1278
1306
|
onProcessingChanged: onProcessingChanged,
|
|
1279
1307
|
onSessionAvailable: function (session) {
|
|
1308
|
+
if (_pairResultRecovery) _pairResultRecovery.available(session);
|
|
1280
1309
|
if (_scheduledMessages) _scheduledMessages.notify();
|
|
1281
1310
|
if (_userMessage && session) setImmediate(function () { _userMessage.consumePendingMessage(session); });
|
|
1282
1311
|
},
|
|
@@ -1741,6 +1770,8 @@ function createProjectContext(opts) {
|
|
|
1741
1770
|
if (msg.type === "memory_delete") { _memory.handleMemoryDelete(ws, msg); return; }
|
|
1742
1771
|
|
|
1743
1772
|
// --- Sessions, config, project mgmt (delegated to project-sessions.js) ---
|
|
1773
|
+
if (_pairResultRecovery.handleMessage(ws, msg)) return;
|
|
1774
|
+
if (_pairWorkerClose.handleMessage(ws, msg)) return;
|
|
1744
1775
|
if (_sessionPair.handleMessage(ws, msg)) return;
|
|
1745
1776
|
if (_sessionHandoff.handleMessage(ws, msg)) return;
|
|
1746
1777
|
if (_driverContinuation.handleMessage(ws, msg)) return;
|
|
@@ -2016,6 +2047,7 @@ function createProjectContext(opts) {
|
|
|
2016
2047
|
serverAuthToken: serverAuthToken,
|
|
2017
2048
|
resolveDefaultAi: opts.resolveDefaultAi,
|
|
2018
2049
|
});
|
|
2050
|
+
_pairResultRecovery.reconcile();
|
|
2019
2051
|
var _toolLlmRuntime = attachProjectToolLlmRuntime({
|
|
2020
2052
|
users: usersModule,
|
|
2021
2053
|
osUsers: osUsers,
|
|
@@ -2066,7 +2098,11 @@ function createProjectContext(opts) {
|
|
|
2066
2098
|
if (isMate || !_sessionPair.workerPermission) return false;
|
|
2067
2099
|
return _sessionPair.workerPermission.isDriverOperated(session);
|
|
2068
2100
|
},
|
|
2069
|
-
beginHumanPairTurn: function (session) {
|
|
2101
|
+
beginHumanPairTurn: function (session) {
|
|
2102
|
+
var resumed = _sessionPair.beginHumanTurn(session);
|
|
2103
|
+
if (resumed) _pairResultRecovery.beginHumanTurn(session);
|
|
2104
|
+
return resumed;
|
|
2105
|
+
},
|
|
2070
2106
|
authorizePendingDispatch: function (session, actor, item) {
|
|
2071
2107
|
if (!session || sm.sessions.get(session.localId) !== session || !item || item.projectSlug !== slug) return false;
|
|
2072
2108
|
if (_scheduledMessages && !_scheduledMessages.authorizePending(item, session)) return false;
|
|
@@ -2499,6 +2535,7 @@ function createProjectContext(opts) {
|
|
|
2499
2535
|
_email: _email,
|
|
2500
2536
|
restoreDebateState: restoreDebateState,
|
|
2501
2537
|
scheduledMessages: _scheduledMessages,
|
|
2538
|
+
pairResultRecovery: _pairResultRecovery,
|
|
2502
2539
|
sendCursorSharingState: opts.sendCursorSharingState,
|
|
2503
2540
|
stopFileWatch: stopFileWatch,
|
|
2504
2541
|
stopAllDirWatches: stopAllDirWatches,
|
package/lib/public/app.js
CHANGED
|
@@ -1387,6 +1387,18 @@ pre.mermaid-error {
|
|
|
1387
1387
|
.plan-banner.plan-exit { color: var(--success); }
|
|
1388
1388
|
.plan-banner.plan-exit .plan-banner-icon { color: var(--success); }
|
|
1389
1389
|
|
|
1390
|
+
.pair-result-status { display: grid; gap: 8px; padding: 10px 16px 0; }
|
|
1391
|
+
.pair-result-status-card { border: 1px solid var(--border); border-radius: 10px; background: var(--bg-alt); padding: 10px 12px; color: var(--text-secondary); font-size: 12px; }
|
|
1392
|
+
.pair-result-status-card > strong, .pair-result-status-card > span { display: block; }
|
|
1393
|
+
.pair-result-status-card > strong { color: var(--text); margin-bottom: 3px; }
|
|
1394
|
+
.pair-result-status-card details { margin-top: 7px; }
|
|
1395
|
+
.pair-result-status-card summary { cursor: pointer; color: var(--accent); }
|
|
1396
|
+
.pair-result-status-card pre { max-height: 180px; overflow: auto; white-space: pre-wrap; font: inherit; }
|
|
1397
|
+
.pair-result-status-card button { margin-top: 8px; }
|
|
1398
|
+
.pair-result-status-retry:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
1399
|
+
.pair-result-status-retry:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
1400
|
+
.pair-result-status-uncertain, .pair-result-status-blocked { border-color: color-mix(in srgb, var(--warning) 45%, var(--border)); }
|
|
1401
|
+
|
|
1390
1402
|
/* --- Plan card --- */
|
|
1391
1403
|
.plan-card {
|
|
1392
1404
|
max-width: var(--content-width);
|
package/lib/public/css/pane.css
CHANGED
|
@@ -73,6 +73,28 @@ body.pane-mode #input-area {
|
|
|
73
73
|
display: flex;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/* A configured two-Worker group is a deliberate workspace hierarchy: the
|
|
77
|
+
Driver gets the broad working canvas, while both Workers remain visible as
|
|
78
|
+
equal-height execution surfaces on the right. Legacy/ad-hoc two-pane
|
|
79
|
+
splits keep the ordinary equal columns below. */
|
|
80
|
+
#split-host:has(.split-pane-role-worker:nth-of-type(3)) {
|
|
81
|
+
display: grid;
|
|
82
|
+
grid-template-columns: minmax(0, 3fr) minmax(260px, 2fr);
|
|
83
|
+
grid-template-rows: repeat(2, minmax(0, 1fr));
|
|
84
|
+
gap: 1px;
|
|
85
|
+
background: var(--border-subtle);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#split-host:has(.split-pane-role-worker:nth-of-type(3)) .split-pane-role-driver {
|
|
89
|
+
grid-row: 1 / span 2;
|
|
90
|
+
grid-column: 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#split-host:has(.split-pane-role-worker:nth-of-type(3)) .split-pane-role-worker {
|
|
94
|
+
grid-column: 2;
|
|
95
|
+
grid-row: var(--split-worker-row, auto);
|
|
96
|
+
}
|
|
97
|
+
|
|
76
98
|
.split-pane {
|
|
77
99
|
flex: 1;
|
|
78
100
|
display: flex;
|
|
@@ -441,6 +463,11 @@ body.pane-mode #input-area {
|
|
|
441
463
|
color: var(--text);
|
|
442
464
|
}
|
|
443
465
|
|
|
466
|
+
.split-pane-close:disabled {
|
|
467
|
+
opacity: 0.42;
|
|
468
|
+
cursor: not-allowed;
|
|
469
|
+
}
|
|
470
|
+
|
|
444
471
|
.split-pane-close svg {
|
|
445
472
|
width: 12px;
|
|
446
473
|
height: 12px;
|
|
@@ -478,6 +505,25 @@ body.pane-mode #input-area {
|
|
|
478
505
|
--split-worker-height: calc(30% - 4.8px);
|
|
479
506
|
}
|
|
480
507
|
|
|
508
|
+
#split-host:has(.split-pane-role-worker:nth-of-type(3)) {
|
|
509
|
+
display: flex;
|
|
510
|
+
flex-direction: column;
|
|
511
|
+
gap: var(--split-pane-gap);
|
|
512
|
+
background: transparent;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
#split-host:has(.split-pane-role-worker:nth-of-type(3)) .split-pane-role-driver,
|
|
516
|
+
#split-host:has(.split-pane-role-worker:nth-of-type(3)) .split-pane-role-worker {
|
|
517
|
+
grid-row: auto;
|
|
518
|
+
grid-column: auto;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
#split-host:has(.split-pane-role-worker:nth-of-type(3)) .split-pane-role-worker {
|
|
522
|
+
flex: 0 1 24%;
|
|
523
|
+
min-height: 104px;
|
|
524
|
+
order: -1;
|
|
525
|
+
}
|
|
526
|
+
|
|
481
527
|
#split-host.visible {
|
|
482
528
|
flex-direction: column;
|
|
483
529
|
gap: var(--split-pane-gap);
|
|
@@ -495,11 +541,11 @@ body.pane-mode #input-area {
|
|
|
495
541
|
}
|
|
496
542
|
|
|
497
543
|
.split-pane-role-worker {
|
|
498
|
-
order: -1;
|
|
544
|
+
order: var(--split-pane-order, -1);
|
|
499
545
|
}
|
|
500
546
|
|
|
501
547
|
.split-pane-role-driver {
|
|
502
|
-
order: 0;
|
|
548
|
+
order: var(--split-pane-order, 0);
|
|
503
549
|
}
|
|
504
550
|
|
|
505
551
|
/* A configured Worker is an execution summary; the Driver keeps the
|