clay-server 3.4.0-beta.2 → 3.4.0-beta.21
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-projects.js +3 -3
- package/lib/daemon.js +59 -59
- package/lib/project-connection.js +35 -7
- package/lib/project-debate.js +4 -0
- package/lib/project-mate-interaction.js +21 -1
- package/lib/project-memory.js +3 -0
- package/lib/project-models.js +220 -0
- package/lib/project-session-handoff.js +162 -0
- package/lib/project-session-pair.js +14 -7
- package/lib/project-session-spawn.js +1 -1
- package/lib/project-sessions.js +13 -45
- package/lib/project-worker-proposal.js +51 -14
- package/lib/project.js +38 -80
- package/lib/public/antigravity-avatar.png +0 -0
- package/lib/public/app.js +29 -0
- package/lib/public/copilot-avatar.svg +5 -0
- package/lib/public/css/command-palette.css +22 -2
- package/lib/public/css/icon-strip.css +29 -13
- package/lib/public/css/input.css +56 -0
- package/lib/public/css/mates.css +6 -0
- package/lib/public/css/menus.css +38 -25
- package/lib/public/css/messages.css +9 -0
- package/lib/public/css/pane.css +16 -3
- package/lib/public/css/pwa-mobile.css +17 -0
- package/lib/public/css/session-actions.css +98 -0
- package/lib/public/css/title-bar.css +19 -0
- package/lib/public/grok-avatar.svg +4 -0
- package/lib/public/index.html +29 -7
- package/lib/public/junie-avatar.svg +11 -0
- package/lib/public/kimi-avatar.svg +4 -0
- package/lib/public/modules/agent-config-selects.js +69 -0
- package/lib/public/modules/app-header.js +4 -22
- package/lib/public/modules/app-messages.js +54 -11
- package/lib/public/modules/app-panels.js +36 -83
- package/lib/public/modules/app-projects.js +3 -1
- package/lib/public/modules/app-rendering.js +19 -4
- package/lib/public/modules/background-tasks-ui.js +55 -0
- package/lib/public/modules/mate-sidebar.js +11 -3
- package/lib/public/modules/model-picker.js +263 -0
- package/lib/public/modules/notifications.js +7 -1
- package/lib/public/modules/pane-bridge.js +11 -0
- package/lib/public/modules/pane-links.js +23 -0
- package/lib/public/modules/project-switcher.js +7 -6
- package/lib/public/modules/session-actions.js +294 -0
- package/lib/public/modules/sidebar-mates.js +1 -1
- package/lib/public/modules/sidebar-mobile.js +2 -1
- package/lib/public/modules/sidebar-projects.js +34 -43
- package/lib/public/modules/sidebar-sessions.js +20 -6
- package/lib/public/modules/split-pair-ui.js +16 -77
- package/lib/public/modules/split-view.js +3 -0
- package/lib/public/modules/tools.js +6 -1
- package/lib/public/modules/vendor-priority.js +14 -0
- package/lib/public/modules/vendor-selection.js +20 -0
- package/lib/public/modules/worker-proposal.js +6 -1
- package/lib/public/modules/worktree-location.js +17 -0
- package/lib/public/qwen-avatar.svg +4 -0
- package/lib/public/style.css +4 -2
- package/lib/sdk-bridge.js +103 -56
- package/lib/sdk-message-processor.js +26 -4
- package/lib/session-handoff-context.js +110 -0
- package/lib/session-notes-mcp-server.js +8 -1
- package/lib/sessions.js +4 -0
- package/lib/worktree.js +9 -4
- package/lib/ws-schema.js +9 -1
- package/lib/yoke/acp-agent-profiles.js +64 -14
- package/lib/yoke/adapters/antigravity.js +417 -0
- package/lib/yoke/adapters/claude.js +34 -0
- package/lib/yoke/adapters/codex.js +101 -5
- package/lib/yoke/adapters/copilot.js +7 -0
- package/lib/yoke/adapters/grok.js +7 -0
- package/lib/yoke/adapters/junie.js +7 -0
- package/lib/yoke/adapters/kimi.js +7 -0
- package/lib/yoke/adapters/kiro.js +2 -2
- package/lib/yoke/adapters/qwen.js +7 -0
- package/lib/yoke/codex-app-server.js +25 -8
- package/lib/yoke/index.js +67 -28
- package/lib/yoke/instructions.js +3 -0
- package/lib/yoke/interface.js +12 -0
- package/lib/yoke/vendor-registry.js +61 -6
- package/package.json +1 -1
- package/lib/public/gemini-avatar.svg +0 -11
- package/lib/yoke/adapters/gemini.js +0 -7
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
var yoke = require("./yoke");
|
|
2
|
+
var contextBuilder = require("./session-handoff-context");
|
|
3
|
+
|
|
4
|
+
function hasUserContext(session) {
|
|
5
|
+
var history = (session && session.history) || [];
|
|
6
|
+
for (var i = 0; i < history.length; i++) {
|
|
7
|
+
if (history[i] && history[i].type === "user_message" && history[i].text) return true;
|
|
8
|
+
if (history[i] && history[i].type === "handoff_context" && history[i].request) return true;
|
|
9
|
+
}
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function attachSessionHandoff(ctx) {
|
|
14
|
+
var sm = ctx.sm;
|
|
15
|
+
|
|
16
|
+
function sendResult(ws, ok, details) {
|
|
17
|
+
ctx.sendTo(ws, Object.assign({ type: "session_handoff_result", ok: ok }, details || {}));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function validate(ws, source, targetVendor) {
|
|
21
|
+
if (ctx.isMate) throw new Error("Session handoff is only available in projects.");
|
|
22
|
+
if (!source) throw new Error("No active session was found.");
|
|
23
|
+
if (ctx.splitStore && ctx.splitStore.groupForMember(source.localId)) {
|
|
24
|
+
throw new Error("Open the session by itself before continuing it in another agent.");
|
|
25
|
+
}
|
|
26
|
+
if (source.isProcessing || source._queryStarting) {
|
|
27
|
+
throw new Error("Wait for the current response to finish before continuing in another agent.");
|
|
28
|
+
}
|
|
29
|
+
if (!hasUserContext(source)) throw new Error("This session does not have any conversation to continue.");
|
|
30
|
+
if (!targetVendor) throw new Error("Choose an agent for the handoff.");
|
|
31
|
+
if (!ctx.adapters[targetVendor] || (sm.installedVendors || []).indexOf(targetVendor) === -1) {
|
|
32
|
+
throw new Error("The selected coding agent is not installed.");
|
|
33
|
+
}
|
|
34
|
+
var linuxUser = ctx.getLinuxUserForSession(source);
|
|
35
|
+
var vendorInfo = yoke.getVendorInfo(targetVendor);
|
|
36
|
+
if (linuxUser && vendorInfo && vendorInfo.osUserIsolation === false) {
|
|
37
|
+
throw new Error((vendorInfo.displayName || targetVendor) + " is not available for OS-isolated users.");
|
|
38
|
+
}
|
|
39
|
+
var ownerId = ws._clayUser && ctx.usersModule.isMultiUser() ? ws._clayUser.id : null;
|
|
40
|
+
if ((source.ownerId || null) !== ownerId) throw new Error("Session access denied.");
|
|
41
|
+
return linuxUser;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function quietRecord(session, entry) {
|
|
45
|
+
session.history.push(entry);
|
|
46
|
+
sm.appendToSessionFile(session, entry);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function handoff(ws, msg) {
|
|
50
|
+
var source = sm.sessions.get(ws._clayActiveSession) || null;
|
|
51
|
+
var targetVendor = typeof msg.targetVendor === "string" ? msg.targetVendor.trim() : "";
|
|
52
|
+
var targetModel = typeof msg.model === "string" ? msg.model.trim() : "";
|
|
53
|
+
var linuxUser;
|
|
54
|
+
try {
|
|
55
|
+
linuxUser = validate(ws, source, targetVendor);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
sendResult(ws, false, { error: err.message || String(err) });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
var targetEffort = yoke.clampEffort(
|
|
62
|
+
targetVendor,
|
|
63
|
+
msg.effort || (sm.currentEffortByVendor && sm.currentEffortByVendor[targetVendor]) || sm.currentEffort || "medium"
|
|
64
|
+
) || null;
|
|
65
|
+
var target = sm.createSessionRaw({
|
|
66
|
+
ownerId: source.ownerId || null,
|
|
67
|
+
sessionVisibility: source.sessionVisibility || "shared",
|
|
68
|
+
vendor: targetVendor,
|
|
69
|
+
model: targetModel || null,
|
|
70
|
+
effort: targetEffort,
|
|
71
|
+
});
|
|
72
|
+
var targetName = (yoke.getVendorInfo(targetVendor) || {}).displayName || targetVendor;
|
|
73
|
+
target.title = (source.title || "Continued session") + " · " + targetName;
|
|
74
|
+
target.handoff = {
|
|
75
|
+
sourceSessionId: source.localId,
|
|
76
|
+
sourceVendor: source.vendor || sm.defaultVendor || "claude",
|
|
77
|
+
createdAt: Date.now(),
|
|
78
|
+
mode: "context",
|
|
79
|
+
sourceHistoryIndex: source.history.length,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
var sourceEntry = {
|
|
83
|
+
type: "handoff_created",
|
|
84
|
+
targetSessionId: target.localId,
|
|
85
|
+
targetVendor: targetVendor,
|
|
86
|
+
targetTitle: target.title,
|
|
87
|
+
_ts: Date.now(),
|
|
88
|
+
};
|
|
89
|
+
var targetEntry = {
|
|
90
|
+
type: "handoff_context",
|
|
91
|
+
sourceSessionId: source.localId,
|
|
92
|
+
sourceVendor: target.handoff.sourceVendor,
|
|
93
|
+
sourceTitle: source.title || "Untitled session",
|
|
94
|
+
targetVendor: targetVendor,
|
|
95
|
+
request: contextBuilder.latestUserRequest(source.history),
|
|
96
|
+
_ts: Date.now(),
|
|
97
|
+
};
|
|
98
|
+
quietRecord(source, sourceEntry);
|
|
99
|
+
quietRecord(target, targetEntry);
|
|
100
|
+
|
|
101
|
+
var prompt = contextBuilder.buildHandoffContext({
|
|
102
|
+
cwd: ctx.cwd,
|
|
103
|
+
source: source,
|
|
104
|
+
targetVendor: targetVendor,
|
|
105
|
+
});
|
|
106
|
+
target.isProcessing = true;
|
|
107
|
+
target.lastActivity = Date.now();
|
|
108
|
+
target.sentToolResults = {};
|
|
109
|
+
sm.switchSession(target.localId, ws);
|
|
110
|
+
if (typeof ctx.onProcessingChanged === "function") ctx.onProcessingChanged();
|
|
111
|
+
sendResult(ws, true, {
|
|
112
|
+
sourceSessionId: source.localId,
|
|
113
|
+
targetSessionId: target.localId,
|
|
114
|
+
targetVendor: targetVendor,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
var sdk = ctx.getSdk();
|
|
118
|
+
function failStart(err) {
|
|
119
|
+
target.isProcessing = false;
|
|
120
|
+
sm.sendAndRecord(target, { type: "error", text: "Could not start the handoff session: " + (err.message || String(err)) });
|
|
121
|
+
if (typeof ctx.onProcessingChanged === "function") ctx.onProcessingChanged();
|
|
122
|
+
sm.broadcastSessionList();
|
|
123
|
+
}
|
|
124
|
+
if (!sdk || typeof sdk.startQuery !== "function") {
|
|
125
|
+
failStart(new Error("SDK bridge is not ready."));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
target._queryStartTs = Date.now();
|
|
129
|
+
var startPromise;
|
|
130
|
+
try {
|
|
131
|
+
startPromise = sdk.startQuery(target, prompt, undefined, linuxUser);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
failStart(err);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
Promise.resolve(startPromise).catch(failStart);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function handleMessage(ws, msg) {
|
|
140
|
+
if (msg.type === "handoff_session_options") {
|
|
141
|
+
ctx.sendTo(ws, {
|
|
142
|
+
type: "handoff_session_options",
|
|
143
|
+
installedVendors: sm.installedVendors || [],
|
|
144
|
+
modelsByVendor: sm.modelsByVendor || {},
|
|
145
|
+
capabilitiesByVendor: sm.capabilitiesByVendor || {},
|
|
146
|
+
});
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
if (msg.type === "handoff_session") {
|
|
150
|
+
handoff(ws, msg);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { handleMessage: handleMessage };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
attachSessionHandoff: attachSessionHandoff,
|
|
161
|
+
hasUserContext: hasUserContext,
|
|
162
|
+
};
|
|
@@ -98,10 +98,15 @@ function attachSessionPair(ctx) {
|
|
|
98
98
|
token.delivered = true;
|
|
99
99
|
var failure = token.failure || null;
|
|
100
100
|
var response = token.response || "";
|
|
101
|
-
var text
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
var text;
|
|
102
|
+
if (token.interrupted) {
|
|
103
|
+
text = "[Worker execution interrupted] The user interrupted the Worker mid-turn. Its work is PARTIAL and unverified — do not treat it as finished. Review what was done and decide next steps with the user.";
|
|
104
|
+
} else {
|
|
105
|
+
text = "A Worker task delegated through send_to_partner has finished.\n\n" +
|
|
106
|
+
"Original task:\n" + token.message + "\n\n" +
|
|
107
|
+
(failure ? "Worker error:\n" + failure : "Worker result:\n" + (response || "(No text response was recorded.)")) +
|
|
108
|
+
"\n\nReview the result, verify it as needed, and continue the task.";
|
|
109
|
+
}
|
|
105
110
|
sm.sendAndRecord(caller, {
|
|
106
111
|
type: "user_message",
|
|
107
112
|
text: text,
|
|
@@ -146,6 +151,7 @@ function attachSessionPair(ctx) {
|
|
|
146
151
|
}
|
|
147
152
|
token.response = responseText(partner.history || [], token.startIndex);
|
|
148
153
|
token.failure = errorSince(partner.history || [], token.startIndex);
|
|
154
|
+
token.interrupted = !token.failure && !!partner._lastTurnInterrupted;
|
|
149
155
|
finishDelegation(group, caller, partner, token);
|
|
150
156
|
return resumeDriverWithResult(caller, partner, token);
|
|
151
157
|
}
|
|
@@ -165,8 +171,9 @@ function attachSessionPair(ctx) {
|
|
|
165
171
|
clearInterval(timer);
|
|
166
172
|
finishDelegation(group, caller, partner, token);
|
|
167
173
|
var failure = errorSince(partner.history || [], token.startIndex);
|
|
174
|
+
var interrupted = !failure && !!partner._lastTurnInterrupted;
|
|
168
175
|
resolve({
|
|
169
|
-
status: failure ? "error" : "complete",
|
|
176
|
+
status: failure ? "error" : (interrupted ? "interrupted" : "complete"),
|
|
170
177
|
response: responseText(partner.history || [], token.startIndex),
|
|
171
178
|
error: failure || undefined,
|
|
172
179
|
});
|
|
@@ -260,7 +267,7 @@ function attachSessionPair(ctx) {
|
|
|
260
267
|
var count = Number.isFinite(args.lastTurns) ? Math.floor(args.lastTurns) : 1;
|
|
261
268
|
count = Math.max(1, Math.min(5, count));
|
|
262
269
|
return toolResult({
|
|
263
|
-
status: resolved.partner.isProcessing ? "running" : "idle",
|
|
270
|
+
status: resolved.partner.isProcessing ? "running" : (resolved.partner._lastTurnInterrupted ? "interrupted" : "idle"),
|
|
264
271
|
partnerId: resolved.partner.localId,
|
|
265
272
|
title: resolved.partner.title || "New Session",
|
|
266
273
|
turns: recentTurns(resolved.partner, count),
|
|
@@ -383,7 +390,7 @@ function attachSessionPair(ctx) {
|
|
|
383
390
|
var group = store.groupForMember(session.localId);
|
|
384
391
|
var pairPrompt = "";
|
|
385
392
|
if (group && group.pair && group.pair.driverId === session.localId) {
|
|
386
|
-
pairPrompt = "You are the Driver in a two-agent pair. The tools send_to_partner and read_partner are provided directly to you. Use send_to_partner to delegate concrete bounded work to the Worker. A completed Worker turn leaves the Worker session available for more work. If review or user feedback requires corrections to the Worker's implementation, delegate a follow-up turn to that Worker instead of editing the Worker-owned files yourself. If send_to_partner reports that the Worker is no longer available, create a replacement with spawn_sessions and delegate the remaining implementation rather than taking it over. If a non-waiting delegation finishes later, Clay pushes the result back and resumes you automatically; use read_partner only when you need an interim status check. Integrate and verify the final outcome. Do not search the project for implementations of these tools, and do not delegate work that must be performed sequentially in this same session.";
|
|
393
|
+
pairPrompt = "You are the Driver in a two-agent pair. The tools send_to_partner and read_partner are provided directly to you. Use send_to_partner to delegate concrete bounded work to the Worker. A completed Worker turn leaves the Worker session available for more work. If a Worker turn is interrupted, its work is partial and unverified; review it and decide next steps with the user. If review or user feedback requires corrections to the Worker's implementation, delegate a follow-up turn to that Worker instead of editing the Worker-owned files yourself. If send_to_partner reports that the Worker is no longer available, create a replacement with spawn_sessions and delegate the remaining implementation rather than taking it over. If a non-waiting delegation finishes later, Clay pushes the result back and resumes you automatically; use read_partner only when you need an interim status check. Integrate and verify the final outcome. Do not search the project for implementations of these tools, and do not delegate work that must be performed sequentially in this same session.";
|
|
387
394
|
}
|
|
388
395
|
return [pairPrompt, workerProposal.getSystemPrompt(session)].filter(Boolean).join("\n\n");
|
|
389
396
|
}
|
|
@@ -319,7 +319,7 @@ function attachSessionSpawn(ctx) {
|
|
|
319
319
|
statuses.push({
|
|
320
320
|
localId: session.localId,
|
|
321
321
|
title: session.title || "New Session",
|
|
322
|
-
status: session.isProcessing ? "running" : (hasSessionError(session) ? "error" : "done"),
|
|
322
|
+
status: session.isProcessing ? "running" : (session._lastTurnInterrupted ? "interrupted" : (hasSessionError(session) ? "error" : "done")),
|
|
323
323
|
turnCount: session.turnCount || 0,
|
|
324
324
|
lastActivity: session.lastActivity || session.createdAt || 0,
|
|
325
325
|
});
|
package/lib/project-sessions.js
CHANGED
|
@@ -658,10 +658,6 @@ function attachSessions(ctx) {
|
|
|
658
658
|
// (resume_tui_session), and born-GUI never auto-converts to a terminal.
|
|
659
659
|
resolveSessionForView(xmTarget, ws);
|
|
660
660
|
}
|
|
661
|
-
// If the target session's vendor doesn't own the currently cached
|
|
662
|
-
// model, clear sm.currentModel so the UI and next query don't leak
|
|
663
|
-
// the previous session's vendor-specific model into this one. A
|
|
664
|
-
// session-specific model always wins and survives daemon restarts.
|
|
665
661
|
var switchTargetSess = sm.sessions.get(msg.id);
|
|
666
662
|
var switchTargetVendor = switchTargetSess && (switchTargetSess.vendor || sm.defaultVendor || "claude");
|
|
667
663
|
if (switchTargetSess && !switchTargetSess.effort) {
|
|
@@ -671,22 +667,6 @@ function attachSessions(ctx) {
|
|
|
671
667
|
) || null;
|
|
672
668
|
sm.saveSessionFile(switchTargetSess);
|
|
673
669
|
}
|
|
674
|
-
if (switchTargetSess && switchTargetSess.model) {
|
|
675
|
-
sm.currentModel = switchTargetSess.model;
|
|
676
|
-
} else if (switchTargetSess && sm.currentModel) {
|
|
677
|
-
var targetVendor = switchTargetSess.vendor || sm.defaultVendor || null;
|
|
678
|
-
var tvModels = (targetVendor && sm.modelsByVendor && sm.modelsByVendor[targetVendor]) || [];
|
|
679
|
-
var found = false;
|
|
680
|
-
var _curLc = sm.currentModel.toLowerCase();
|
|
681
|
-
for (var tvi = 0; tvi < tvModels.length; tvi++) {
|
|
682
|
-
var tvEntry = tvModels[tvi];
|
|
683
|
-
var tvVal = typeof tvEntry === "string" ? tvEntry : (tvEntry && (tvEntry.value || tvEntry.id)) || "";
|
|
684
|
-
if (tvVal === sm.currentModel || (tvVal && (tvVal.toLowerCase().indexOf(_curLc) !== -1 || _curLc.indexOf(tvVal.toLowerCase()) !== -1))) { found = true; break; }
|
|
685
|
-
}
|
|
686
|
-
if (tvModels.length > 0 && !found) {
|
|
687
|
-
sm.currentModel = "";
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
670
|
// Check access in multi-user mode
|
|
691
671
|
if (usersModule.isMultiUser() && ws._clayUser) {
|
|
692
672
|
var switchTarget = sm.sessions.get(msg.id);
|
|
@@ -958,14 +938,6 @@ function attachSessions(ctx) {
|
|
|
958
938
|
return true;
|
|
959
939
|
}
|
|
960
940
|
|
|
961
|
-
if (msg.type === "set_model" && msg.model) {
|
|
962
|
-
var session = getSessionForWs(ws);
|
|
963
|
-
if (session) {
|
|
964
|
-
sdk.setModel(session, msg.model);
|
|
965
|
-
}
|
|
966
|
-
return true;
|
|
967
|
-
}
|
|
968
|
-
|
|
969
941
|
if (msg.type === "reload_skills") {
|
|
970
942
|
var session = getSessionForWs(ws);
|
|
971
943
|
if (session && sdk.reloadSkills) {
|
|
@@ -997,11 +969,7 @@ function attachSessions(ctx) {
|
|
|
997
969
|
" is bound to '" + vendorSession.vendor + "', refused rebind to '" + msg.vendor + "'");
|
|
998
970
|
} else {
|
|
999
971
|
vendorSession.vendor = msg.vendor;
|
|
1000
|
-
|
|
1001
|
-
// instead of leaking the previous vendor's model into a fresh session.
|
|
1002
|
-
if (sm.currentModel) {
|
|
1003
|
-
sm.currentModel = "";
|
|
1004
|
-
}
|
|
972
|
+
if (vendorSession.vendor !== msg.vendor) vendorSession.model = "";
|
|
1005
973
|
sm.saveSessionFile(vendorSession);
|
|
1006
974
|
sm.broadcastSessionList();
|
|
1007
975
|
}
|
|
@@ -1012,11 +980,11 @@ function attachSessions(ctx) {
|
|
|
1012
980
|
type: "model_info",
|
|
1013
981
|
model: "",
|
|
1014
982
|
models: vendorModels,
|
|
1015
|
-
vendor: msg.vendor,
|
|
983
|
+
vendor: msg.vendor, sessionId: vendorSession ? vendorSession.localId : null,
|
|
1016
984
|
availableVendors: sm.availableVendors || [],
|
|
1017
985
|
installedVendors: sm.installedVendors || [],
|
|
1018
986
|
});
|
|
1019
|
-
send({ type: "config_state",
|
|
987
|
+
send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1020
988
|
}
|
|
1021
989
|
return true;
|
|
1022
990
|
}
|
|
@@ -1052,7 +1020,7 @@ function attachSessions(ctx) {
|
|
|
1052
1020
|
sm.broadcastSessionList();
|
|
1053
1021
|
sdk.setPermissionMode(session, msg.mode);
|
|
1054
1022
|
}
|
|
1055
|
-
send({ type: "config_state",
|
|
1023
|
+
send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1056
1024
|
return true;
|
|
1057
1025
|
}
|
|
1058
1026
|
|
|
@@ -1113,7 +1081,7 @@ function attachSessions(ctx) {
|
|
|
1113
1081
|
if (session) {
|
|
1114
1082
|
sdk.setPermissionMode(session, msg.mode);
|
|
1115
1083
|
}
|
|
1116
|
-
send({ type: "config_state",
|
|
1084
|
+
send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1117
1085
|
return true;
|
|
1118
1086
|
}
|
|
1119
1087
|
|
|
@@ -1126,7 +1094,7 @@ function attachSessions(ctx) {
|
|
|
1126
1094
|
if (session) {
|
|
1127
1095
|
sdk.setPermissionMode(session, msg.mode);
|
|
1128
1096
|
}
|
|
1129
|
-
send({ type: "config_state",
|
|
1097
|
+
send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1130
1098
|
return true;
|
|
1131
1099
|
}
|
|
1132
1100
|
|
|
@@ -1142,7 +1110,7 @@ function attachSessions(ctx) {
|
|
|
1142
1110
|
sdk.setEffort(session, sessionEffort).catch(function (e) {
|
|
1143
1111
|
sendTo(ws, { type: "error", text: "Failed to change reasoning effort: " + (e.message || e) });
|
|
1144
1112
|
});
|
|
1145
|
-
sm.sendToSession(session, { type: "config_state", model: session.model || sm.
|
|
1113
|
+
sm.sendToSession(session, { type: "config_state", model: session.model || ((sm.defaultModelByVendor || {})[effortVendor]) || "", vendor: effortVendor, sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: sessionEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1146
1114
|
}
|
|
1147
1115
|
return true;
|
|
1148
1116
|
}
|
|
@@ -1152,7 +1120,7 @@ function attachSessions(ctx) {
|
|
|
1152
1120
|
opts.onSetServerDefaultEffort(msg.effort);
|
|
1153
1121
|
}
|
|
1154
1122
|
sm.currentEffort = msg.effort;
|
|
1155
|
-
send({ type: "config_state",
|
|
1123
|
+
send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1156
1124
|
return true;
|
|
1157
1125
|
}
|
|
1158
1126
|
|
|
@@ -1161,20 +1129,20 @@ function attachSessions(ctx) {
|
|
|
1161
1129
|
opts.onSetProjectDefaultEffort(slug, msg.effort);
|
|
1162
1130
|
}
|
|
1163
1131
|
sm.currentEffort = msg.effort;
|
|
1164
|
-
send({ type: "config_state",
|
|
1132
|
+
send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1165
1133
|
return true;
|
|
1166
1134
|
}
|
|
1167
1135
|
|
|
1168
1136
|
if (msg.type === "set_betas") {
|
|
1169
1137
|
sm.currentBetas = msg.betas || [];
|
|
1170
|
-
send({ type: "config_state",
|
|
1138
|
+
send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas, thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1171
1139
|
return true;
|
|
1172
1140
|
}
|
|
1173
1141
|
|
|
1174
1142
|
if (msg.type === "set_thinking") {
|
|
1175
1143
|
sm.currentThinking = msg.thinking || "adaptive";
|
|
1176
1144
|
if (msg.budgetTokens) sm.currentThinkingBudget = msg.budgetTokens;
|
|
1177
|
-
send({ type: "config_state",
|
|
1145
|
+
send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1178
1146
|
return true;
|
|
1179
1147
|
}
|
|
1180
1148
|
|
|
@@ -1456,7 +1424,7 @@ function attachSessions(ctx) {
|
|
|
1456
1424
|
if (decision === "allow_accept_edits") {
|
|
1457
1425
|
sdk.setPermissionMode(session, "acceptEdits");
|
|
1458
1426
|
sm.currentPermissionMode = "acceptEdits";
|
|
1459
|
-
send({ type: "config_state",
|
|
1427
|
+
send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1460
1428
|
pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
|
|
1461
1429
|
sm.sendAndRecord(session, { type: "permission_resolved", requestId: requestId, decision: decision });
|
|
1462
1430
|
return true;
|
|
@@ -1485,7 +1453,7 @@ function attachSessions(ctx) {
|
|
|
1485
1453
|
|
|
1486
1454
|
// Update permission mode for the new session
|
|
1487
1455
|
sm.currentPermissionMode = "acceptEdits";
|
|
1488
|
-
send({ type: "config_state",
|
|
1456
|
+
send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1489
1457
|
|
|
1490
1458
|
// Build prompt from plan content (sent from client) or plan file path
|
|
1491
1459
|
var clientPlanContent = msg.planContent || "";
|
|
@@ -40,7 +40,7 @@ function attachWorkerProposal(ctx) {
|
|
|
40
40
|
function isEligible(session) {
|
|
41
41
|
if (ctx.isMate || !session || session.mode === "tui") return false;
|
|
42
42
|
if (store.groupForMember(session.localId)) return false;
|
|
43
|
-
return isFableSession(session, sm.modelsByVendor, sm.
|
|
43
|
+
return isFableSession(session, sm.modelsByVendor, (sm.defaultModelByVendor || {})[session.vendor || "claude"]);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function safeModelsByVendor(installed) {
|
|
@@ -112,7 +112,7 @@ function attachWorkerProposal(ctx) {
|
|
|
112
112
|
}
|
|
113
113
|
var models = options.modelsByVendor[vendor] || [];
|
|
114
114
|
var model = modelIsAvailable(options, vendor, args.recommendedModel) ? (args.recommendedModel || "") : "";
|
|
115
|
-
var currentModel = session.model || sm.
|
|
115
|
+
var currentModel = session.model || (sm.defaultModelByVendor || {})[session.vendor || "claude"] || "";
|
|
116
116
|
if (vendor === session.vendor && model === currentModel) model = "";
|
|
117
117
|
if (!model && models.length > 0) {
|
|
118
118
|
for (var j = 0; j < models.length; j++) {
|
|
@@ -154,6 +154,18 @@ function attachWorkerProposal(ctx) {
|
|
|
154
154
|
}, patch));
|
|
155
155
|
}
|
|
156
156
|
|
|
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
|
+
|
|
157
169
|
async function propose(args, session) {
|
|
158
170
|
if (!isEligible(session)) return toolResult({ error: "Worker suggestions are only available in an unpaired Fable session." });
|
|
159
171
|
if (hasPendingProposal(session)) return toolResult({ error: "A Worker suggestion is already awaiting a decision." });
|
|
@@ -166,6 +178,7 @@ function attachWorkerProposal(ctx) {
|
|
|
166
178
|
var options = proposalOptions();
|
|
167
179
|
if (options.installedVendors.length === 0) return toolResult({ error: "No coding agent is installed for a Worker session." });
|
|
168
180
|
var recommendation = chooseRecommendation(args, session, options);
|
|
181
|
+
var autoApprove = skipPermissionsEnabled(session);
|
|
169
182
|
var proposal = {
|
|
170
183
|
type: "worker_proposal",
|
|
171
184
|
proposalId: "worker_" + crypto.randomUUID(),
|
|
@@ -178,7 +191,22 @@ function attachWorkerProposal(ctx) {
|
|
|
178
191
|
recommendedEffort: recommendation.effort,
|
|
179
192
|
options: options,
|
|
180
193
|
};
|
|
194
|
+
if (autoApprove) proposal.autoApproved = true;
|
|
181
195
|
sm.sendAndRecord(session, proposal);
|
|
196
|
+
if (autoApprove) {
|
|
197
|
+
var accepted = await acceptProposal(session, proposal, {
|
|
198
|
+
vendor: recommendation.vendor,
|
|
199
|
+
model: recommendation.model,
|
|
200
|
+
effort: recommendation.effort,
|
|
201
|
+
autoApproved: true,
|
|
202
|
+
}, autoApprovalWs(session));
|
|
203
|
+
if (!accepted.ok) return toolResult({ error: accepted.error || "Could not start the Worker." });
|
|
204
|
+
return toolResult({
|
|
205
|
+
status: "running",
|
|
206
|
+
proposalId: proposal.proposalId,
|
|
207
|
+
instruction: "The Worker was auto-approved and started because skip permissions is enabled. Its result will return for review.",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
182
210
|
return toolResult({
|
|
183
211
|
status: "posted",
|
|
184
212
|
proposalId: proposal.proposalId,
|
|
@@ -220,6 +248,8 @@ function attachWorkerProposal(ctx) {
|
|
|
220
248
|
var followup;
|
|
221
249
|
if (result.status === "complete") {
|
|
222
250
|
followup = "[Worker execution completed]\nReview and verify the Worker's result. The 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 Worker-owned files yourself. If that Worker is no longer available, create a replacement with spawn_sessions for the remaining implementation.\n\n" + (result.response || "The Worker completed without a text summary.");
|
|
251
|
+
} else if (result.status === "interrupted") {
|
|
252
|
+
followup = "[Worker execution interrupted]\nThe user interrupted the Worker mid-turn. Its work is PARTIAL and unverified — do not treat it as finished. Review what was done and decide next steps with the user.";
|
|
223
253
|
} else if (result.status === "running") {
|
|
224
254
|
followup = "[Worker execution is still running]\nUse read_partner to inspect progress before completing the task.";
|
|
225
255
|
} else {
|
|
@@ -238,30 +268,24 @@ function attachWorkerProposal(ctx) {
|
|
|
238
268
|
return session;
|
|
239
269
|
}
|
|
240
270
|
|
|
241
|
-
async function
|
|
242
|
-
var session = sessionForResponse(ws);
|
|
243
|
-
var proposal = findProposal(session, msg.proposalId);
|
|
244
|
-
if (!proposal) throw new Error("Worker suggestion not found");
|
|
245
|
-
if (proposal.status !== "pending") throw new Error("Worker suggestion has already been resolved");
|
|
246
|
-
if (!msg.accepted) {
|
|
247
|
-
updateProposal(session, proposal, { status: "declined" });
|
|
248
|
-
await resumeDriver(session, "[Worker suggestion declined]\nContinue this task in the current session using the plan you already prepared.");
|
|
249
|
-
return { ok: true, status: "declined" };
|
|
250
|
-
}
|
|
271
|
+
async function acceptProposal(session, proposal, msg, ws) {
|
|
251
272
|
var options = proposal.options || proposalOptions();
|
|
252
273
|
var vendor = msg.vendor || proposal.recommendedVendor;
|
|
253
274
|
var model = msg.model || "";
|
|
254
275
|
if (options.installedVendors.indexOf(vendor) === -1) throw new Error("Selected Worker vendor is not installed");
|
|
255
276
|
if (!modelIsAvailable(options, vendor, model)) throw new Error("Selected Worker model is unavailable");
|
|
256
277
|
var effort = yoke.clampEffort(vendor, msg.effort || proposal.recommendedEffort || "medium") || "";
|
|
257
|
-
|
|
278
|
+
var startingPatch = { status: "starting", selectedVendor: vendor, selectedModel: model, selectedEffort: effort };
|
|
279
|
+
if (msg.autoApproved) startingPatch.autoApproved = true;
|
|
280
|
+
updateProposal(session, proposal, startingPatch);
|
|
258
281
|
try {
|
|
259
282
|
var created = ctx.createPairRecord(ws, {
|
|
260
283
|
driver: { sessionId: session.localId },
|
|
261
284
|
worker: { vendor: vendor, model: model, effort: effort },
|
|
262
285
|
});
|
|
263
286
|
updateProposal(session, proposal, { status: "running", groupId: created.group.id, workerId: created.worker.localId });
|
|
264
|
-
|
|
287
|
+
if (ws._autoApproval) sm.sendToSession(session, { type: "pair_session_created", ok: true, group: created.group });
|
|
288
|
+
else ctx.sendTo(ws, { type: "pair_session_created", ok: true, group: created.group });
|
|
265
289
|
runWorker(session, proposal).catch(function (err) {
|
|
266
290
|
updateProposal(session, proposal, { status: "error", error: err.message || String(err) });
|
|
267
291
|
resumeDriver(session, "[Worker execution failed]\n" + (err.message || String(err))).catch(function () {});
|
|
@@ -273,6 +297,19 @@ function attachWorkerProposal(ctx) {
|
|
|
273
297
|
}
|
|
274
298
|
}
|
|
275
299
|
|
|
300
|
+
async function respondToProposal(ws, msg) {
|
|
301
|
+
var session = sessionForResponse(ws);
|
|
302
|
+
var proposal = findProposal(session, msg.proposalId);
|
|
303
|
+
if (!proposal) throw new Error("Worker suggestion not found");
|
|
304
|
+
if (proposal.status !== "pending") throw new Error("Worker suggestion has already been resolved");
|
|
305
|
+
if (!msg.accepted) {
|
|
306
|
+
updateProposal(session, proposal, { status: "declined" });
|
|
307
|
+
await resumeDriver(session, "[Worker suggestion declined]\nContinue this task in the current session using the plan you already prepared.");
|
|
308
|
+
return { ok: true, status: "declined" };
|
|
309
|
+
}
|
|
310
|
+
return acceptProposal(session, proposal, msg, ws);
|
|
311
|
+
}
|
|
312
|
+
|
|
276
313
|
function handleMessage(ws, msg) {
|
|
277
314
|
if (msg.type !== "worker_proposal_response") return false;
|
|
278
315
|
respondToProposal(ws, msg).catch(function (err) {
|