clay-server 4.0.0-beta.13 → 4.0.0-beta.15
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/capsule-display-floor.js +51 -0
- package/lib/capsule-frame-server.js +229 -0
- package/lib/capsule-pig-logic.js +268 -0
- package/lib/capsule-server-runtimes.js +41 -0
- package/lib/capsule-tictactoe-logic.js +260 -0
- package/lib/capsules/pig/display.js +190 -0
- package/lib/capsules/pig/manifest.json +9 -0
- package/lib/capsules/pig/ui.json +46 -0
- package/lib/capsules/tictactoe/manifest.json +9 -0
- package/lib/capsules/tictactoe/ui.json +203 -0
- package/lib/project-capsule-catalog.js +9 -1
- package/lib/project-connection.js +1 -1
- package/lib/project-log-feedback-delivery.js +76 -0
- package/lib/project-logs.js +4 -0
- package/lib/project-pair-lifecycle.js +7 -12
- package/lib/project-session-pair.js +29 -39
- package/lib/project-worker-proposal.js +203 -72
- package/lib/project.js +13 -0
- package/lib/public/css/capsule-ui.css +18 -0
- package/lib/public/css/home-session-actions.css +66 -0
- package/lib/public/css/home-sidebar.css +66 -0
- package/lib/public/css/mobile-nav.css +80 -0
- package/lib/public/css/sidebar.css +92 -0
- package/lib/public/css/worker-proposal.css +34 -1
- package/lib/public/modules/app-messages.js +14 -1
- package/lib/public/modules/home-conversations-sheet.js +102 -48
- package/lib/public/modules/home-session-actions.js +6 -0
- package/lib/public/modules/home-sidebar-chat-list.js +101 -41
- package/lib/public/modules/home-tool-frame.js +165 -0
- package/lib/public/modules/home-tools.js +129 -1
- package/lib/public/modules/session-hierarchy.js +54 -0
- package/lib/public/modules/sidebar-mobile.js +35 -6
- package/lib/public/modules/sidebar-session-hierarchy.js +206 -0
- package/lib/public/modules/sidebar-sessions.js +49 -7
- package/lib/public/modules/worker-proposal-state.js +16 -0
- package/lib/public/modules/worker-proposal.js +49 -12
- package/lib/sdk-bridge.js +16 -8
- package/lib/sdk-message-processor.js +34 -4
- package/lib/server-home-chat.js +12 -1
- package/lib/server-tools.js +97 -9
- package/lib/server.js +3 -0
- package/lib/session-driver-eligibility.js +20 -165
- package/lib/session-pair-factory.js +31 -28
- package/lib/session-pair-mcp-server.js +5 -9
- package/lib/session-pair-prompts.js +15 -15
- package/lib/session-provenance.js +119 -0
- package/lib/session-spawn-mcp-server.js +1 -1
- package/lib/sessions.js +40 -2
- package/lib/tools-registry.js +44 -10
- package/lib/ws-schema.js +8 -3
- package/lib/yoke/adapters/claude.js +12 -0
- package/package.json +1 -1
|
@@ -1,48 +1,35 @@
|
|
|
1
1
|
var crypto = require("crypto");
|
|
2
2
|
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 MAX_SUMMARY_CHARS = 600;
|
|
6
6
|
var MAX_PLAN_CHARS = 6000;
|
|
7
7
|
var MAX_TASK_CHARS = 30000;
|
|
8
|
-
|
|
8
|
+
var MAX_RATIONALE_CHARS = 1000;
|
|
9
9
|
function modelValue(entry) {
|
|
10
10
|
if (typeof entry === "string") return entry;
|
|
11
11
|
return entry && (entry.value || entry.id) || "";
|
|
12
12
|
}
|
|
13
|
-
|
|
14
13
|
function modelLabel(entry) {
|
|
15
14
|
if (typeof entry === "string") return entry;
|
|
16
15
|
return entry && (entry.displayName || entry.name || entry.value || entry.id) || "";
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
function isFableSession(session, modelsByVendor, fallbackModel) {
|
|
20
|
-
if (!session || (session.vendor || "claude") !== "claude") return false;
|
|
21
|
-
var effectiveModel = session.model || fallbackModel || "";
|
|
22
|
-
var selected = String(effectiveModel).toLowerCase();
|
|
23
|
-
if (selected.indexOf("fable") !== -1) return true;
|
|
24
|
-
var models = (modelsByVendor && modelsByVendor.claude) || [];
|
|
25
|
-
for (var i = 0; i < models.length; i++) {
|
|
26
|
-
if (modelValue(models[i]) !== effectiveModel) continue;
|
|
27
|
-
if (modelLabel(models[i]).toLowerCase().indexOf("fable") !== -1) return true;
|
|
28
|
-
}
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
18
|
function toolResult(value) {
|
|
33
19
|
return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] });
|
|
34
20
|
}
|
|
35
|
-
|
|
36
21
|
function attachWorkerProposal(ctx) {
|
|
37
22
|
var sm = ctx.sm;
|
|
38
23
|
var store = ctx.splitStore;
|
|
24
|
+
function isLiveSession(session) {
|
|
25
|
+
return !!session && sm.sessions.get(session.localId) === session;
|
|
26
|
+
}
|
|
39
27
|
|
|
40
28
|
function isEligible(session) {
|
|
41
|
-
if (ctx.isMate || !session || session
|
|
29
|
+
if (ctx.isMate || !isLiveSession(session) || !driverEligibility.isEligibleDriverSession(session, sm)) return false;
|
|
42
30
|
if (store.groupForMember(session.localId)) return false;
|
|
43
|
-
return
|
|
31
|
+
return true;
|
|
44
32
|
}
|
|
45
|
-
|
|
46
33
|
function safeModelsByVendor(installed) {
|
|
47
34
|
var result = {};
|
|
48
35
|
for (var i = 0; i < installed.length; i++) {
|
|
@@ -95,7 +82,21 @@ function attachWorkerProposal(ctx) {
|
|
|
95
82
|
for (var i = 0; i < models.length; i++) {
|
|
96
83
|
if (modelValue(models[i]) === model) return true;
|
|
97
84
|
}
|
|
98
|
-
return
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function effortIsAvailable(options, vendor, model, effort) {
|
|
89
|
+
var capabilities = options.capabilitiesByVendor[vendor] || {};
|
|
90
|
+
if (capabilities.effort === false) return !effort;
|
|
91
|
+
var clamped = yoke.clampEffort(vendor, effort) || "";
|
|
92
|
+
if (!effort || clamped !== effort) return false;
|
|
93
|
+
var models = options.modelsByVendor[vendor] || [];
|
|
94
|
+
for (var i = 0; i < models.length; i++) {
|
|
95
|
+
if (modelValue(models[i]) !== model) continue;
|
|
96
|
+
var levels = models[i] && models[i].supportedEffortLevels;
|
|
97
|
+
return !Array.isArray(levels) || levels.length === 0 || levels.indexOf(effort) !== -1;
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
function chooseRecommendation(args, session, options) {
|
|
@@ -117,7 +118,6 @@ function attachWorkerProposal(ctx) {
|
|
|
117
118
|
if (!model && models.length > 0) {
|
|
118
119
|
for (var j = 0; j < models.length; j++) {
|
|
119
120
|
if (vendor === session.vendor && modelValue(models[j]) === currentModel) continue;
|
|
120
|
-
if (modelLabel(models[j]).toLowerCase().indexOf("fable") !== -1) continue;
|
|
121
121
|
model = modelValue(models[j]);
|
|
122
122
|
break;
|
|
123
123
|
}
|
|
@@ -127,6 +127,31 @@ function attachWorkerProposal(ctx) {
|
|
|
127
127
|
return { vendor: vendor, model: model, effort: effort };
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
function skipPermissionsEnabled(session) {
|
|
131
|
+
return !!(session && (session.permissionMode === "bypassPermissions" ||
|
|
132
|
+
session.dangerouslySkipPermissions || ctx.dangerouslySkipPermissions));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function recommendationCanAutoAccept(args, recommendation, options) {
|
|
136
|
+
var requestedVendor = typeof args.recommendedVendor === "string" ? args.recommendedVendor.trim() : "";
|
|
137
|
+
var requestedModel = typeof args.recommendedModel === "string" ? args.recommendedModel.trim() : "";
|
|
138
|
+
var requestedEffort = typeof args.recommendedEffort === "string" ? args.recommendedEffort.trim() : "";
|
|
139
|
+
if (!requestedVendor || requestedVendor !== recommendation.vendor) return false;
|
|
140
|
+
if (!requestedModel || requestedModel !== recommendation.model ||
|
|
141
|
+
!modelIsAvailable(options, requestedVendor, requestedModel)) return false;
|
|
142
|
+
var capabilities = options.capabilitiesByVendor[requestedVendor] || {};
|
|
143
|
+
if (capabilities.effort === false) return !requestedEffort && !recommendation.effort;
|
|
144
|
+
return requestedEffort === recommendation.effort &&
|
|
145
|
+
effortIsAvailable(options, requestedVendor, requestedModel, requestedEffort);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function autoAcceptanceWs(session) {
|
|
149
|
+
return {
|
|
150
|
+
_clayActiveSession: session.localId,
|
|
151
|
+
_clayUser: session.ownerId ? { id: session.ownerId } : null,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
130
155
|
function findProposal(session, proposalId) {
|
|
131
156
|
var history = (session && session.history) || [];
|
|
132
157
|
for (var i = history.length - 1; i >= 0; i--) {
|
|
@@ -154,31 +179,24 @@ function attachWorkerProposal(ctx) {
|
|
|
154
179
|
}, patch));
|
|
155
180
|
}
|
|
156
181
|
|
|
157
|
-
function skipPermissionsEnabled(session) {
|
|
158
|
-
return !!session && (session.permissionMode === "bypassPermissions" || session.dangerouslySkipPermissions === true);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function autoApprovalWs(session) {
|
|
162
|
-
return {
|
|
163
|
-
_clayActiveSession: session.localId,
|
|
164
|
-
_clayUser: session.ownerId ? { id: session.ownerId } : null,
|
|
165
|
-
_autoApproval: true,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
182
|
async function propose(args, session) {
|
|
170
|
-
if (!isEligible(session)) return toolResult({ error: "Split Worker
|
|
183
|
+
if (!isEligible(session)) return toolResult({ error: "Split Worker proposals are only available in an eligible unpaired Driver session." });
|
|
171
184
|
if (hasPendingProposal(session)) return toolResult({ error: "A Split Worker suggestion is already awaiting a decision." });
|
|
172
185
|
var summary = typeof args.summary === "string" ? args.summary.trim() : "";
|
|
173
186
|
var plan = typeof args.plan === "string" ? args.plan.trim() : "";
|
|
174
187
|
var task = typeof args.message === "string" ? args.message.trim() : "";
|
|
175
|
-
|
|
188
|
+
var rationale = typeof args.recommendationRationale === "string" ? args.recommendationRationale.trim() : "";
|
|
189
|
+
if (!summary || !plan || !task || !rationale) {
|
|
190
|
+
return toolResult({ error: "summary, plan, message, and recommendationRationale are required." });
|
|
191
|
+
}
|
|
176
192
|
if (task.length > MAX_TASK_CHARS) return toolResult({ error: "The Split Worker task is too long." });
|
|
177
193
|
await ensureModelCatalogs();
|
|
194
|
+
if (!isEligible(session)) {
|
|
195
|
+
return toolResult({ error: "The Driver session changed while Split Worker runtimes were loading." });
|
|
196
|
+
}
|
|
178
197
|
var options = proposalOptions();
|
|
179
198
|
if (options.installedVendors.length === 0) return toolResult({ error: "No coding agent is installed for a Split Worker session." });
|
|
180
199
|
var recommendation = chooseRecommendation(args, session, options);
|
|
181
|
-
var autoApprove = skipPermissionsEnabled(session);
|
|
182
200
|
var proposal = {
|
|
183
201
|
type: "worker_proposal",
|
|
184
202
|
proposalId: "worker_" + crypto.randomUUID(),
|
|
@@ -189,22 +207,22 @@ function attachWorkerProposal(ctx) {
|
|
|
189
207
|
recommendedVendor: recommendation.vendor,
|
|
190
208
|
recommendedModel: recommendation.model,
|
|
191
209
|
recommendedEffort: recommendation.effort,
|
|
210
|
+
recommendationRationale: rationale.slice(0, MAX_RATIONALE_CHARS),
|
|
192
211
|
options: options,
|
|
193
212
|
};
|
|
194
|
-
if (autoApprove) proposal.autoApproved = true;
|
|
195
213
|
sm.sendAndRecord(session, proposal);
|
|
196
|
-
if (
|
|
214
|
+
if (skipPermissionsEnabled(session) && recommendationCanAutoAccept(args, recommendation, options)) {
|
|
197
215
|
var accepted = await acceptProposal(session, proposal, {
|
|
198
216
|
vendor: recommendation.vendor,
|
|
199
217
|
model: recommendation.model,
|
|
200
218
|
effort: recommendation.effort,
|
|
201
|
-
|
|
202
|
-
}, autoApprovalWs(session));
|
|
203
|
-
if (!accepted.ok) return toolResult({ error: accepted.error || "Could not start the Split Worker." });
|
|
219
|
+
}, autoAcceptanceWs(session), true);
|
|
204
220
|
return toolResult({
|
|
205
|
-
status: "
|
|
221
|
+
status: accepted.ok ? "auto_accepted" : "posted",
|
|
206
222
|
proposalId: proposal.proposalId,
|
|
207
|
-
instruction:
|
|
223
|
+
instruction: accepted.ok
|
|
224
|
+
? "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."
|
|
225
|
+
: "The automatic decision failed closed. The configuration card remains pending for the user; end this turn and wait for their decision.",
|
|
208
226
|
});
|
|
209
227
|
}
|
|
210
228
|
return toolResult({
|
|
@@ -214,6 +232,75 @@ function attachWorkerProposal(ctx) {
|
|
|
214
232
|
});
|
|
215
233
|
}
|
|
216
234
|
|
|
235
|
+
async function proposeReplacement(args, session) {
|
|
236
|
+
var group = session && store.groupForMember(session.localId);
|
|
237
|
+
if (ctx.isMate || !isLiveSession(session) || !driverEligibility.isEligibleDriverSession(session, sm) || !group ||
|
|
238
|
+
!group.pair || group.pair.driverId !== session.localId) {
|
|
239
|
+
return toolResult({ error: "Split Worker replacement proposals require the exact paired Driver session." });
|
|
240
|
+
}
|
|
241
|
+
var sourceGroupId = group.id;
|
|
242
|
+
var sourceWorkerId = group.pair.workerId;
|
|
243
|
+
if (hasPendingProposal(session)) return toolResult({ error: "A Split Worker proposal is already awaiting a decision." });
|
|
244
|
+
var task = typeof args.message === "string" ? args.message.trim() : "";
|
|
245
|
+
var rationale = typeof args.recommendationRationale === "string" ? args.recommendationRationale.trim() : "";
|
|
246
|
+
if (!task) return toolResult({ error: "message is required so an accepted replacement delegates exactly once." });
|
|
247
|
+
if (!rationale) return toolResult({ error: "recommendationRationale is required for the replacement audit trail." });
|
|
248
|
+
if (task.length > MAX_TASK_CHARS) return toolResult({ error: "The Split Worker task is too long." });
|
|
249
|
+
await ensureModelCatalogs();
|
|
250
|
+
var liveGroup = store.groupForMember(session.localId);
|
|
251
|
+
if (!isLiveSession(session) || !driverEligibility.isEligibleDriverSession(session, sm) ||
|
|
252
|
+
!liveGroup || liveGroup.id !== sourceGroupId || !liveGroup.pair ||
|
|
253
|
+
liveGroup.pair.driverId !== session.localId || liveGroup.pair.workerId !== sourceWorkerId) {
|
|
254
|
+
return toolResult({ error: "The Driver/Split Worker pair changed while replacement runtimes were loading." });
|
|
255
|
+
}
|
|
256
|
+
var options = proposalOptions();
|
|
257
|
+
if (options.installedVendors.length === 0) return toolResult({ error: "No coding agent is installed for a Split Worker session." });
|
|
258
|
+
var recommendationArgs = {
|
|
259
|
+
recommendedVendor: args.workerVendor,
|
|
260
|
+
recommendedModel: args.workerModel,
|
|
261
|
+
recommendedEffort: args.workerEffort,
|
|
262
|
+
};
|
|
263
|
+
var recommendation = chooseRecommendation(recommendationArgs, session, options);
|
|
264
|
+
var proposal = {
|
|
265
|
+
type: "worker_proposal",
|
|
266
|
+
proposalId: "worker_" + crypto.randomUUID(),
|
|
267
|
+
action: "replace",
|
|
268
|
+
summary: "Replace the current Split Worker before the next delegated task.",
|
|
269
|
+
plan: "1. Preserve the current Worker's session and history\n2. Create the selected replacement runtime\n3. Delegate the next task exactly once",
|
|
270
|
+
message: task,
|
|
271
|
+
status: "pending",
|
|
272
|
+
recommendedVendor: recommendation.vendor,
|
|
273
|
+
recommendedModel: recommendation.model,
|
|
274
|
+
recommendedEffort: recommendation.effort,
|
|
275
|
+
recommendationRationale: rationale.slice(0, MAX_RATIONALE_CHARS),
|
|
276
|
+
options: options,
|
|
277
|
+
sourceGroupId: sourceGroupId,
|
|
278
|
+
sourceWorkerId: sourceWorkerId,
|
|
279
|
+
interrupt: args.interrupt === true,
|
|
280
|
+
evaluation: args.evaluation || null,
|
|
281
|
+
};
|
|
282
|
+
sm.sendAndRecord(session, proposal);
|
|
283
|
+
if (skipPermissionsEnabled(session) && recommendationCanAutoAccept(recommendationArgs, recommendation, options)) {
|
|
284
|
+
var accepted = await acceptProposal(session, proposal, {
|
|
285
|
+
vendor: recommendation.vendor,
|
|
286
|
+
model: recommendation.model,
|
|
287
|
+
effort: recommendation.effort,
|
|
288
|
+
}, autoAcceptanceWs(session), true);
|
|
289
|
+
return toolResult({
|
|
290
|
+
status: accepted.ok ? "auto_accepted" : "posted",
|
|
291
|
+
proposalId: proposal.proposalId,
|
|
292
|
+
instruction: accepted.ok
|
|
293
|
+
? "The recorded replacement configuration was auto-accepted under the Driver session's full-access mode."
|
|
294
|
+
: "The automatic replacement failed closed. The card remains pending for the user.",
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return toolResult({
|
|
298
|
+
status: "posted",
|
|
299
|
+
proposalId: proposal.proposalId,
|
|
300
|
+
instruction: "The replacement configuration is visible in the chat. End this turn now and wait for the user's decision.",
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
217
304
|
function resumeDriver(session, text) {
|
|
218
305
|
var sdk = ctx.getSdk();
|
|
219
306
|
if (!sdk) return Promise.reject(new Error("SDK bridge is not ready"));
|
|
@@ -268,31 +355,86 @@ function attachWorkerProposal(ctx) {
|
|
|
268
355
|
return session;
|
|
269
356
|
}
|
|
270
357
|
|
|
271
|
-
async function acceptProposal(session, proposal, msg, ws) {
|
|
272
|
-
|
|
358
|
+
async function acceptProposal(session, proposal, msg, ws, trustedAutoAcceptance) {
|
|
359
|
+
if (!isLiveSession(session)) throw new Error("The Driver session is no longer live");
|
|
360
|
+
if (proposal.action !== "replace" && !isEligible(session)) {
|
|
361
|
+
throw new Error("The Driver is no longer eligible to create this Split Worker");
|
|
362
|
+
}
|
|
363
|
+
var approvedGroup = null;
|
|
364
|
+
if (proposal.action === "replace") {
|
|
365
|
+
approvedGroup = store.groupForMember(session.localId);
|
|
366
|
+
if (!driverEligibility.isEligibleDriverSession(session, sm) || !approvedGroup ||
|
|
367
|
+
approvedGroup.id !== proposal.sourceGroupId || !approvedGroup.pair ||
|
|
368
|
+
approvedGroup.pair.driverId !== session.localId || approvedGroup.pair.workerId !== proposal.sourceWorkerId) {
|
|
369
|
+
throw new Error("The Driver/Split Worker pair changed before the replacement was approved");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
var options = proposalOptions();
|
|
273
373
|
var vendor = msg.vendor || proposal.recommendedVendor;
|
|
274
374
|
var model = msg.model || "";
|
|
275
375
|
if (options.installedVendors.indexOf(vendor) === -1) throw new Error("Selected Split Worker vendor is not installed");
|
|
276
376
|
if (!modelIsAvailable(options, vendor, model)) throw new Error("Selected Split Worker model is unavailable");
|
|
277
|
-
var
|
|
278
|
-
var
|
|
279
|
-
if (msg.
|
|
377
|
+
var requestedEffort = msg.effort || proposal.recommendedEffort || "medium";
|
|
378
|
+
var effort = yoke.clampEffort(vendor, requestedEffort) || "";
|
|
379
|
+
if ((msg.effort && effort !== msg.effort) || !effortIsAvailable(options, vendor, model, effort)) {
|
|
380
|
+
throw new Error("Selected Split Worker reasoning effort is unavailable");
|
|
381
|
+
}
|
|
382
|
+
var autoAccepted = trustedAutoAcceptance === true;
|
|
383
|
+
var startingPatch = {
|
|
384
|
+
status: "starting",
|
|
385
|
+
selectedVendor: vendor,
|
|
386
|
+
selectedModel: model,
|
|
387
|
+
selectedEffort: effort,
|
|
388
|
+
autoAccepted: autoAccepted,
|
|
389
|
+
decisionMode: autoAccepted ? "driver_recommendation" : "user",
|
|
390
|
+
decidedAt: Date.now(),
|
|
391
|
+
};
|
|
280
392
|
updateProposal(session, proposal, startingPatch);
|
|
281
393
|
try {
|
|
394
|
+
if (proposal.action === "replace") {
|
|
395
|
+
var liveGroup = approvedGroup;
|
|
396
|
+
var replaced = await ctx.replacePartner({
|
|
397
|
+
interrupt: proposal.interrupt === true,
|
|
398
|
+
workerVendor: vendor,
|
|
399
|
+
workerModel: model,
|
|
400
|
+
workerEffort: effort,
|
|
401
|
+
evaluation: proposal.evaluation || undefined,
|
|
402
|
+
}, session);
|
|
403
|
+
liveGroup = store.groupForMember(session.localId);
|
|
404
|
+
updateProposal(session, proposal, { status: "running", groupId: liveGroup.id, workerId: replaced.workerSessionId });
|
|
405
|
+
runWorker(session, proposal).catch(function (err) {
|
|
406
|
+
updateProposal(session, proposal, { status: "error", error: err.message || String(err) });
|
|
407
|
+
resumeDriver(session, "[Split Worker execution failed]\n" + (err.message || String(err))).catch(function () {});
|
|
408
|
+
});
|
|
409
|
+
return { ok: true, status: "running", group: liveGroup, replacement: replaced };
|
|
410
|
+
}
|
|
282
411
|
var created = ctx.createPairRecord(ws, {
|
|
283
412
|
driver: { sessionId: session.localId },
|
|
284
413
|
worker: { vendor: vendor, model: model, effort: effort },
|
|
285
414
|
});
|
|
415
|
+
ctx.recordGenerationStart(session, created.worker);
|
|
286
416
|
updateProposal(session, proposal, { status: "running", groupId: created.group.id, workerId: created.worker.localId });
|
|
287
|
-
|
|
288
|
-
else ctx.sendTo(ws, { type: "pair_session_created", ok: true, group: created.group });
|
|
417
|
+
ctx.sendTo(ws, { type: "pair_session_created", ok: true, group: created.group });
|
|
289
418
|
runWorker(session, proposal).catch(function (err) {
|
|
290
419
|
updateProposal(session, proposal, { status: "error", error: err.message || String(err) });
|
|
291
420
|
resumeDriver(session, "[Split Worker execution failed]\n" + (err.message || String(err))).catch(function () {});
|
|
292
421
|
});
|
|
293
422
|
return { ok: true, status: "running", group: created.group };
|
|
294
423
|
} catch (err) {
|
|
295
|
-
|
|
424
|
+
if (proposal.action === "replace") {
|
|
425
|
+
var restoredGroup = store.groupForMember(session.localId);
|
|
426
|
+
if (restoredGroup && restoredGroup.pair && restoredGroup.pair.driverId === session.localId &&
|
|
427
|
+
restoredGroup.pair.workerId === proposal.sourceWorkerId) {
|
|
428
|
+
proposal.sourceGroupId = restoredGroup.id;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
updateProposal(session, proposal, {
|
|
432
|
+
status: "pending",
|
|
433
|
+
error: err.message || String(err),
|
|
434
|
+
autoAccepted: false,
|
|
435
|
+
decisionMode: null,
|
|
436
|
+
decidedAt: null,
|
|
437
|
+
});
|
|
296
438
|
return { ok: false, status: "pending", error: err.message || String(err) };
|
|
297
439
|
}
|
|
298
440
|
}
|
|
@@ -304,7 +446,10 @@ function attachWorkerProposal(ctx) {
|
|
|
304
446
|
if (proposal.status !== "pending") throw new Error("Split Worker suggestion has already been resolved");
|
|
305
447
|
if (!msg.accepted) {
|
|
306
448
|
updateProposal(session, proposal, { status: "declined" });
|
|
307
|
-
|
|
449
|
+
var declinedText = proposal.action === "replace"
|
|
450
|
+
? "[Split Worker replacement declined]\nContinue with the existing Split Worker and the current task."
|
|
451
|
+
: "[Split Worker proposal declined]\nContinue this task in the current Driver session using the plan you already prepared.";
|
|
452
|
+
await resumeDriver(session, declinedText);
|
|
308
453
|
return { ok: true, status: "declined" };
|
|
309
454
|
}
|
|
310
455
|
return acceptProposal(session, proposal, msg, ws);
|
|
@@ -318,18 +463,11 @@ function attachWorkerProposal(ctx) {
|
|
|
318
463
|
return true;
|
|
319
464
|
}
|
|
320
465
|
|
|
321
|
-
// Retired as a required path: the tool is no longer offered to any model.
|
|
322
|
-
// Kept in the module (with its handlers and the client message handler) so an
|
|
323
|
-
// older client mid-proposal still resolves compatibly.
|
|
324
466
|
function getToolDefs(session) {
|
|
325
|
-
return [];
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function retiredToolDefs(session) {
|
|
329
467
|
if (!isEligible(session)) return [];
|
|
330
468
|
return [{
|
|
331
469
|
name: "propose_worker",
|
|
332
|
-
description: "
|
|
470
|
+
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.",
|
|
333
471
|
inputSchema: buildShape({
|
|
334
472
|
summary: { type: "string", description: "One short sentence explaining why a Split Worker is useful for this task." },
|
|
335
473
|
plan: { type: "string", description: "A concise numbered implementation plan to show in the approval card." },
|
|
@@ -337,27 +475,21 @@ function attachWorkerProposal(ctx) {
|
|
|
337
475
|
recommendedVendor: { type: "string", description: "Optional installed vendor id for the Split Worker." },
|
|
338
476
|
recommendedModel: { type: "string", description: "Optional exact Split Worker model id. Omit when uncertain." },
|
|
339
477
|
recommendedEffort: { type: "string", description: "Optional reasoning effort: minimal, low, medium, high, xhigh, or max." },
|
|
340
|
-
|
|
478
|
+
recommendationRationale: { type: "string", description: "Concise Driver-authored explanation of why the recommended vendor, model, and effort fit this exact task." },
|
|
479
|
+
}, ["summary", "plan", "message", "recommendationRationale"]),
|
|
341
480
|
handler: function (args) { return propose(args || {}, session); },
|
|
342
481
|
}];
|
|
343
482
|
}
|
|
344
483
|
|
|
345
484
|
function getSystemPrompt(session) {
|
|
346
485
|
if (!isEligible(session)) return "";
|
|
347
|
-
|
|
348
|
-
// own authority, so the model is never told to propose one. The tool and
|
|
349
|
-
// its message handler remain so an older client's in-flight proposal frame
|
|
350
|
-
// still resolves instead of erroring.
|
|
351
|
-
return "";
|
|
486
|
+
return "For implementation-heavy work, use propose_worker before substantial execution. 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.";
|
|
352
487
|
}
|
|
353
488
|
|
|
354
489
|
return {
|
|
355
490
|
getToolDefs: getToolDefs,
|
|
356
|
-
// The retired definitions, retained only so an older client's in-flight
|
|
357
|
-
// proposal still resolves and so that compatibility path stays testable.
|
|
358
|
-
// Never mounted for any model: getToolDefs returns nothing.
|
|
359
|
-
retiredToolDefs: retiredToolDefs,
|
|
360
491
|
getSystemPrompt: getSystemPrompt,
|
|
492
|
+
proposeReplacement: proposeReplacement,
|
|
361
493
|
handleMessage: handleMessage,
|
|
362
494
|
respondToProposal: respondToProposal,
|
|
363
495
|
};
|
|
@@ -365,5 +497,4 @@ function attachWorkerProposal(ctx) {
|
|
|
365
497
|
|
|
366
498
|
module.exports = {
|
|
367
499
|
attachWorkerProposal: attachWorkerProposal,
|
|
368
|
-
isFableSession: isFableSession,
|
|
369
500
|
};
|
package/lib/project.js
CHANGED
|
@@ -51,9 +51,11 @@ var { attachSessionDocument } = require("./project-session-document");
|
|
|
51
51
|
var { attachCapsuleCatalog } = require("./project-capsule-catalog");
|
|
52
52
|
var { attachProjectWorkspaceQuery } = require("./project-workspace-query");
|
|
53
53
|
var { attachProjectLogs } = require("./project-logs");
|
|
54
|
+
var { attachProjectLogFeedbackDelivery } = require("./project-log-feedback-delivery");
|
|
54
55
|
var { attachProjectMateKnowledge } = require("./project-mate-knowledge");
|
|
55
56
|
var { attachSplitGroups } = require("./session-split-groups");
|
|
56
57
|
var toolControlMcp = require("./tool-control-mcp-server");
|
|
58
|
+
var capsuleFloor = require("./capsule-display-floor");
|
|
57
59
|
var toolLlm = require("./tool-llm");
|
|
58
60
|
// project-notifications is attached globally in server.js, passed via opts.notificationsModule
|
|
59
61
|
|
|
@@ -64,6 +66,10 @@ function createMateToolControlMcp(adapter, config) {
|
|
|
64
66
|
var manifests = config.list(config.userId) || [];
|
|
65
67
|
var tools = [];
|
|
66
68
|
for (var i = 0; i < manifests.length; i++) {
|
|
69
|
+
// Skills go dark when the declarative Display floor does, so a Capsule
|
|
70
|
+
// the human cannot operate is never listed to a Mate either. The guard
|
|
71
|
+
// validates the Capsule's own Display tree, not a claim about it.
|
|
72
|
+
if (!capsuleFloor.hasUsableFloor(manifests[i])) continue;
|
|
67
73
|
tools.push({
|
|
68
74
|
id: manifests[i].id,
|
|
69
75
|
name: manifests[i].name,
|
|
@@ -670,6 +676,12 @@ function createProjectContext(opts) {
|
|
|
670
676
|
isMate: isMate,
|
|
671
677
|
mateId: mateId,
|
|
672
678
|
});
|
|
679
|
+
var _logFeedbackDelivery = attachProjectLogFeedbackDelivery({
|
|
680
|
+
sm: sm,
|
|
681
|
+
getSdk: function () { return sdk; },
|
|
682
|
+
onProcessingChanged: onProcessingChanged,
|
|
683
|
+
getLinuxUserForSession: getLinuxUserForSession,
|
|
684
|
+
});
|
|
673
685
|
var _projectLogs = attachProjectLogs({
|
|
674
686
|
service: opts.projectLogsService || null,
|
|
675
687
|
sm: sm,
|
|
@@ -679,6 +691,7 @@ function createProjectContext(opts) {
|
|
|
679
691
|
mateId: mateId,
|
|
680
692
|
sendTo: sendTo,
|
|
681
693
|
getClients: function () { return clients; },
|
|
694
|
+
onFeedback: _logFeedbackDelivery.deliver,
|
|
682
695
|
});
|
|
683
696
|
var _mateKnowledge = attachProjectMateKnowledge({
|
|
684
697
|
service: opts.mateKnowledgeService || null,
|
|
@@ -384,6 +384,24 @@ textarea.tool-input { min-height: 92px; resize: vertical; }
|
|
|
384
384
|
.tool-pagination { flex-wrap: wrap; justify-content: flex-start; }
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
/* Rich Display frame: the additive sandboxed element above the floor. */
|
|
388
|
+
.home-tool-frame-wrap { width: 100%; }
|
|
389
|
+
.home-tool-frame { display: block; width: 100%; min-height: 240px; border: 0; border-radius: 10px; background: var(--bg); }
|
|
390
|
+
.home-tool-frame-toggle {
|
|
391
|
+
display: block; margin: 6px 0 0 auto; padding: 3px 10px;
|
|
392
|
+
border: 1px solid var(--border-subtle); border-radius: 999px;
|
|
393
|
+
background: transparent; color: var(--text-muted); font-size: 11px; cursor: pointer;
|
|
394
|
+
}
|
|
395
|
+
.home-tool-frame-toggle:hover { color: var(--text); border-color: var(--accent); }
|
|
396
|
+
|
|
397
|
+
/* Host-side attribution flash when the other seat's act changes state. */
|
|
398
|
+
@keyframes capsule-remote-act-flash {
|
|
399
|
+
0% { box-shadow: 0 0 0 2px var(--accent); }
|
|
400
|
+
100% { box-shadow: 0 0 0 2px transparent; }
|
|
401
|
+
}
|
|
402
|
+
.capsule-remote-act { animation: capsule-remote-act-flash 0.8s ease-out; }
|
|
403
|
+
|
|
387
404
|
@media (prefers-reduced-motion: reduce) {
|
|
388
405
|
.tool-button, .tool-input, .tool-select { transition: none; }
|
|
406
|
+
.capsule-remote-act { animation: none; }
|
|
389
407
|
}
|
|
@@ -22,6 +22,72 @@
|
|
|
22
22
|
.home-sidebar-recent-item .home-sidebar-recent-row:hover,
|
|
23
23
|
.home-conversations-sheet-item .home-conversations-sheet-row:hover { background: transparent; }
|
|
24
24
|
|
|
25
|
+
.home-conversations-driver-header {
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: stretch;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.home-conversations-driver-header > .home-conversations-sheet-item {
|
|
31
|
+
min-width: 0;
|
|
32
|
+
flex: 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.home-conversations-driver-toggle {
|
|
36
|
+
width: 32px;
|
|
37
|
+
padding: 0;
|
|
38
|
+
border: 0;
|
|
39
|
+
border-radius: 8px;
|
|
40
|
+
display: inline-flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
flex-shrink: 0;
|
|
44
|
+
color: var(--text-dimmer);
|
|
45
|
+
background: transparent;
|
|
46
|
+
cursor: pointer;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.home-conversations-driver-toggle:hover,
|
|
50
|
+
.home-conversations-driver-toggle:focus-visible {
|
|
51
|
+
color: var(--text);
|
|
52
|
+
background: rgba(var(--overlay-rgb), 0.05);
|
|
53
|
+
outline: none;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.home-conversations-driver-toggle .lucide {
|
|
57
|
+
width: 14px;
|
|
58
|
+
height: 14px;
|
|
59
|
+
transition: transform 0.15s ease;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.home-conversations-driver-toggle[aria-expanded="true"] .lucide {
|
|
63
|
+
transform: rotate(90deg);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.home-conversations-driver-header:not(:has(.home-conversations-sheet-item)) .home-conversations-driver-toggle {
|
|
67
|
+
width: 100%;
|
|
68
|
+
min-height: 38px;
|
|
69
|
+
padding: 0 10px;
|
|
70
|
+
justify-content: flex-start;
|
|
71
|
+
gap: 9px;
|
|
72
|
+
font: inherit;
|
|
73
|
+
font-size: 11px;
|
|
74
|
+
text-align: left;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.home-conversations-driver-header:not(:has(.home-conversations-sheet-item)) .home-conversations-driver-toggle span:first-of-type {
|
|
78
|
+
flex: 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.home-conversations-worker-children {
|
|
82
|
+
margin-left: 16px;
|
|
83
|
+
padding-left: 10px;
|
|
84
|
+
border-left: 1px solid color-mix(in srgb, var(--text-dimmer) 22%, transparent);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.home-conversations-worker-item .home-conversations-sheet-row {
|
|
88
|
+
padding-block: 8px;
|
|
89
|
+
}
|
|
90
|
+
|
|
25
91
|
.home-sidebar-recent-row:focus-visible,
|
|
26
92
|
.home-conversations-sheet-row:focus-visible {
|
|
27
93
|
position: relative;
|
|
@@ -478,6 +478,72 @@
|
|
|
478
478
|
line-height: 1.45;
|
|
479
479
|
}
|
|
480
480
|
|
|
481
|
+
.home-sidebar-driver-header {
|
|
482
|
+
display: flex;
|
|
483
|
+
align-items: stretch;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.home-sidebar-driver-header > .home-sidebar-recent-item {
|
|
487
|
+
min-width: 0;
|
|
488
|
+
flex: 1;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.home-sidebar-driver-toggle {
|
|
492
|
+
width: 27px;
|
|
493
|
+
padding: 0;
|
|
494
|
+
border: 0;
|
|
495
|
+
border-radius: 7px;
|
|
496
|
+
display: inline-flex;
|
|
497
|
+
align-items: center;
|
|
498
|
+
justify-content: center;
|
|
499
|
+
flex-shrink: 0;
|
|
500
|
+
color: var(--text-dimmer);
|
|
501
|
+
background: transparent;
|
|
502
|
+
cursor: pointer;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.home-sidebar-driver-toggle:hover,
|
|
506
|
+
.home-sidebar-driver-toggle:focus-visible {
|
|
507
|
+
color: var(--text);
|
|
508
|
+
background: rgba(var(--overlay-rgb), 0.05);
|
|
509
|
+
outline: none;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.home-sidebar-driver-toggle .lucide {
|
|
513
|
+
width: 13px;
|
|
514
|
+
height: 13px;
|
|
515
|
+
transition: transform 0.15s ease;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.home-sidebar-driver-toggle[aria-expanded="true"] .lucide {
|
|
519
|
+
transform: rotate(90deg);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.home-sidebar-worker-children {
|
|
523
|
+
margin-left: 13px;
|
|
524
|
+
padding-left: 8px;
|
|
525
|
+
border-left: 1px solid color-mix(in srgb, var(--text-dimmer) 22%, transparent);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.home-sidebar-worker-item .home-sidebar-recent-row {
|
|
529
|
+
min-height: 38px;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.home-sidebar-orphan-toggle {
|
|
533
|
+
width: 100%;
|
|
534
|
+
min-height: 34px;
|
|
535
|
+
padding: 0 8px;
|
|
536
|
+
justify-content: flex-start;
|
|
537
|
+
gap: 7px;
|
|
538
|
+
font: inherit;
|
|
539
|
+
font-size: 11px;
|
|
540
|
+
text-align: left;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.home-sidebar-orphan-toggle span:first-of-type {
|
|
544
|
+
flex: 1;
|
|
545
|
+
}
|
|
546
|
+
|
|
481
547
|
.home-sidebar-expand {
|
|
482
548
|
position: absolute;
|
|
483
549
|
top: 8px;
|