clay-server 4.0.0-beta.2 → 4.0.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/background-task-timing.js +105 -0
- package/lib/capsule-display-floor.js +51 -0
- package/lib/capsule-frame-server.js +255 -0
- package/lib/capsule-mate-turn.js +105 -0
- package/lib/capsule-pig-logic.js +300 -0
- package/lib/capsule-server-runtimes.js +39 -0
- package/lib/capsules/pig/display.js +198 -0
- package/lib/capsules/pig/manifest.json +9 -0
- package/lib/capsules/pig/ui.json +84 -0
- package/lib/config.js +25 -1
- package/lib/daemon-projects.js +13 -8
- package/lib/daemon.js +127 -14
- package/lib/git-cli.js +95 -1
- package/lib/knowledge-import.js +436 -0
- package/lib/knowledge-record-store.js +188 -0
- package/lib/knowledge-search.js +240 -0
- package/lib/mate-knowledge-mcp-server.js +151 -0
- package/lib/mate-knowledge-migration.js +283 -0
- package/lib/mate-knowledge-service.js +488 -0
- package/lib/mate-knowledge-sync.js +482 -0
- package/lib/migrate-single-user.js +2 -0
- package/lib/notes-lifecycle.js +149 -0
- package/lib/notes.js +60 -3
- package/lib/os-users.js +3 -1
- package/lib/project-capsule-catalog.js +9 -1
- package/lib/project-capsule-turn.js +96 -0
- package/lib/project-connection.js +14 -1
- package/lib/project-http.js +15 -1
- package/lib/project-knowledge.js +22 -0
- package/lib/project-log-context.js +172 -0
- package/lib/project-log-feedback-delivery.js +118 -0
- package/lib/project-logs-comments.js +137 -0
- package/lib/project-logs-context-state.js +42 -0
- package/lib/project-logs-mcp-server.js +328 -0
- package/lib/project-logs-query.js +185 -0
- package/lib/project-logs-root.js +75 -0
- package/lib/project-logs-schema.js +259 -0
- package/lib/project-logs-service.js +463 -0
- package/lib/project-logs-snapshot.js +205 -0
- package/lib/project-logs-store.js +495 -0
- package/lib/project-logs-versioning.js +132 -0
- package/lib/project-logs.js +400 -0
- package/lib/project-mate-interaction.js +14 -0
- package/lib/project-mate-knowledge.js +110 -0
- package/lib/project-memory.js +22 -0
- package/lib/project-message-delivery.js +70 -0
- package/lib/project-pair-lifecycle.js +460 -0
- package/lib/project-session-handoff.js +1 -1
- package/lib/project-session-notes.js +64 -13
- package/lib/project-session-pair.js +159 -146
- package/lib/project-session-spawn.js +1 -1
- package/lib/project-sessions.js +58 -13
- package/lib/project-user-message.js +53 -4
- package/lib/project-vendor-login.js +20 -3
- package/lib/project-worker-permission.js +430 -0
- package/lib/project-worker-proposal.js +208 -62
- package/lib/project.js +178 -6
- package/lib/public/app.js +58 -35
- package/lib/public/css/admin.css +2 -2
- package/lib/public/css/capsule-ui.css +38 -0
- package/lib/public/css/command-palette.css +19 -9
- package/lib/public/css/git-placard.css +154 -0
- package/lib/public/css/home-hub.css +4 -0
- package/lib/public/css/home-session-actions.css +66 -0
- package/lib/public/css/home-sidebar.css +66 -0
- package/lib/public/css/input.css +163 -28
- package/lib/public/css/mobile-nav.css +80 -0
- package/lib/public/css/notifications-center.css +67 -0
- package/lib/public/css/pane.css +52 -0
- package/lib/public/css/project-logs.css +198 -0
- package/lib/public/css/scheduler.css +1 -1
- package/lib/public/css/session-actions.css +16 -101
- package/lib/public/css/sidebar.css +103 -0
- package/lib/public/css/sticky-notes.css +237 -229
- package/lib/public/css/worker-proposal.css +34 -1
- package/lib/public/index.html +45 -11
- package/lib/public/modules/admin.js +8 -8
- package/lib/public/modules/app-connection.js +109 -4
- package/lib/public/modules/app-home-hub.js +19 -7
- package/lib/public/modules/app-message-cards.js +2 -1
- package/lib/public/modules/app-messages.js +84 -7
- package/lib/public/modules/app-misc.js +2 -14
- package/lib/public/modules/app-notifications.js +11 -7
- package/lib/public/modules/app-panels.js +46 -4
- package/lib/public/modules/app-projects.js +13 -8
- package/lib/public/modules/app-rendering.js +1 -0
- package/lib/public/modules/background-tasks-ui.js +139 -16
- package/lib/public/modules/capsule-preference.js +44 -0
- package/lib/public/modules/chat-bubble-renderer.js +1 -1
- package/lib/public/modules/clay-log-links.js +70 -0
- package/lib/public/modules/command-palette.js +52 -15
- package/lib/public/modules/context-sources.js +0 -3
- package/lib/public/modules/git-agent-sessions.js +85 -0
- package/lib/public/modules/git-panel.js +111 -84
- package/lib/public/modules/git-placard.js +161 -0
- package/lib/public/modules/home-chat-empty-state.js +39 -0
- package/lib/public/modules/home-chat-identity.js +9 -0
- package/lib/public/modules/home-conversations-sheet.js +102 -48
- package/lib/public/modules/home-mate-chat.js +15 -35
- 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-sidebar.js +7 -0
- package/lib/public/modules/home-tool-frame.js +182 -0
- package/lib/public/modules/home-tools.js +138 -1
- package/lib/public/modules/input.js +38 -7
- package/lib/public/modules/markdown.js +2 -0
- package/lib/public/modules/mate-sidebar.js +0 -8
- package/lib/public/modules/message-delivery.js +97 -0
- package/lib/public/modules/paste-modal.js +84 -0
- package/lib/public/modules/project-logs-render.js +405 -0
- package/lib/public/modules/project-logs.js +461 -0
- package/lib/public/modules/project-removal-target.js +26 -0
- package/lib/public/modules/scheduler.js +17 -3
- package/lib/public/modules/session-actions.js +29 -105
- package/lib/public/modules/session-hierarchy.js +54 -0
- package/lib/public/modules/sidebar-mobile.js +40 -15
- package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
- package/lib/public/modules/sidebar-sessions.js +77 -11
- package/lib/public/modules/sidebar.js +17 -5
- package/lib/public/modules/split-group-helpers.js +8 -0
- package/lib/public/modules/split-view.js +5 -2
- package/lib/public/modules/sticky-note-markdown.js +5 -0
- package/lib/public/modules/sticky-notes-browser.js +326 -0
- package/lib/public/modules/sticky-notes-card.js +360 -0
- package/lib/public/modules/sticky-notes-editor.js +291 -0
- package/lib/public/modules/sticky-notes-shared.js +90 -0
- package/lib/public/modules/sticky-notes.js +170 -946
- package/lib/public/modules/tool-palette-order.js +143 -0
- package/lib/public/modules/tool-palette-overlays.js +159 -0
- package/lib/public/modules/tool-palette.js +21 -188
- package/lib/public/modules/tools.js +21 -1
- package/lib/public/modules/update-snooze.js +169 -0
- package/lib/public/modules/user-settings.js +4 -0
- package/lib/public/modules/worker-pane-lock.js +176 -0
- package/lib/public/modules/worker-proposal-state.js +16 -0
- package/lib/public/modules/worker-proposal.js +49 -12
- package/lib/public/style.css +3 -1
- package/lib/sdk-bridge.js +132 -46
- package/lib/sdk-message-processor.js +60 -14
- package/lib/server-experimental-settings.js +49 -0
- package/lib/server-global-ws.js +11 -0
- package/lib/server-home-chat-events.js +31 -1
- package/lib/server-home-chat.js +27 -4
- package/lib/server-home-clay-entry.js +1 -1
- package/lib/server-home-clay-session-links.js +34 -5
- package/lib/server-settings.js +5 -0
- package/lib/server-tools.js +127 -9
- package/lib/server.js +124 -18
- package/lib/session-driver-eligibility.js +38 -0
- package/lib/session-driver-orchestration.js +31 -0
- package/lib/session-hygiene.js +3 -0
- package/lib/session-notes-mcp-server.js +25 -5
- package/lib/session-pair-factory.js +247 -0
- package/lib/session-pair-mcp-server.js +46 -8
- package/lib/session-pair-prompts.js +79 -0
- package/lib/session-pair-turn-control.js +153 -0
- package/lib/session-provenance.js +119 -0
- package/lib/session-spawn-mcp-server.js +1 -1
- package/lib/session-split-groups.js +4 -1
- package/lib/session-title-policy.js +60 -0
- package/lib/session-visibility.js +39 -0
- package/lib/sessions.js +61 -11
- package/lib/tools-registry.js +89 -10
- package/lib/update-snooze.js +437 -0
- package/lib/users-experimental-preferences.js +30 -0
- package/lib/users-permissions.js +2 -2
- package/lib/users.js +22 -16
- package/lib/workspace-query-access.js +102 -0
- package/lib/workspace-query-service.js +24 -17
- package/lib/worktree.js +25 -38
- package/lib/ws-schema.js +38 -7
- package/lib/yoke/adapters/claude-worker.js +16 -6
- package/lib/yoke/adapters/claude.js +81 -4
- package/lib/yoke/adapters/codex.js +63 -15
- package/lib/yoke/codex-background-tasks.js +8 -2
- package/lib/yoke/vendor-registry.js +1 -1
- package/package.json +3 -3
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
var pairMcp = require("./session-pair-mcp-server");
|
|
2
|
+
var pairPrompts = require("./session-pair-prompts");
|
|
3
|
+
var { attachPairFactory } = require("./session-pair-factory");
|
|
4
|
+
var { attachPairLifecycle } = require("./project-pair-lifecycle");
|
|
5
|
+
var { attachPairTurnControl } = require("./session-pair-turn-control");
|
|
6
|
+
var driverEligibility = require("./session-driver-eligibility"), driverOrchestration = require("./session-driver-orchestration");
|
|
7
|
+
var { attachWorkerPermission } = require("./project-worker-permission");
|
|
2
8
|
var { attachWorkerProposal } = require("./project-worker-proposal");
|
|
3
|
-
var yoke = require("./yoke");
|
|
4
|
-
|
|
5
9
|
var MAX_RESPONSE_CHARS = 30000;
|
|
6
|
-
|
|
7
|
-
function toolResult(value) {
|
|
8
|
-
return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] });
|
|
9
|
-
}
|
|
10
|
-
|
|
10
|
+
function toolResult(value) { return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] }); }
|
|
11
11
|
function toolError(err) {
|
|
12
12
|
return Promise.resolve({
|
|
13
13
|
content: [{ type: "text", text: "Error: " + (err.message || err) }],
|
|
14
14
|
isError: true,
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
|
-
|
|
18
17
|
function responseText(history, fromIndex) {
|
|
19
18
|
var text = "";
|
|
20
19
|
for (var i = Math.max(0, fromIndex || 0); i < history.length; i++) {
|
|
@@ -59,11 +58,17 @@ function attachSessionPair(ctx) {
|
|
|
59
58
|
var sm = ctx.sm;
|
|
60
59
|
var store = ctx.splitStore;
|
|
61
60
|
var workerProposal;
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
var workerPermission;
|
|
62
|
+
var lifecycle;
|
|
63
|
+
var turnControl = attachPairTurnControl({ sm: sm, splitStore: store });
|
|
64
|
+
function groupAndPartner(caller, missingPairMessage) {
|
|
64
65
|
if (!caller) throw new Error("partner tools require a session-bound tool server");
|
|
66
|
+
// Captured handlers must remain bound to the exact live Session object.
|
|
67
|
+
if (sm.sessions.get(caller.localId) !== caller) {
|
|
68
|
+
throw new Error("this session is no longer live; the partner tools are bound to an exact session");
|
|
69
|
+
}
|
|
65
70
|
var group = store.groupForMember(caller.localId);
|
|
66
|
-
if (!group) throw new Error("this session is not in a split group");
|
|
71
|
+
if (!group) throw new Error(missingPairMessage || "this session is not in a split group");
|
|
67
72
|
if (group.pair && group.pair.driverId !== caller.localId) {
|
|
68
73
|
throw new Error("only the configured Driver can direct this pair");
|
|
69
74
|
}
|
|
@@ -93,27 +98,19 @@ function attachSessionPair(ctx) {
|
|
|
93
98
|
broadcastDelegation(group, caller, partner, false);
|
|
94
99
|
}
|
|
95
100
|
|
|
96
|
-
function
|
|
97
|
-
if (!
|
|
98
|
-
|
|
99
|
-
var
|
|
100
|
-
var response = token.response || "";
|
|
101
|
-
var text;
|
|
102
|
-
if (token.interrupted) {
|
|
103
|
-
text = "[Split Worker execution interrupted] The user interrupted the Split 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 Split Worker task delegated through send_to_partner has finished.\n\n" +
|
|
106
|
-
"Original task:\n" + token.message + "\n\n" +
|
|
107
|
-
(failure ? "Split Worker error:\n" + failure : "Split Worker result:\n" + (response || "(No text response was recorded.)")) +
|
|
108
|
-
"\n\nReview the result, verify it as needed, and continue the task.";
|
|
109
|
-
}
|
|
110
|
-
sm.sendAndRecord(caller, {
|
|
101
|
+
function resumeDriverWithMessage(caller, text, meta) {
|
|
102
|
+
if (!caller || caller.destroying) return false;
|
|
103
|
+
if (turnControl.blockedReason(caller)) return false;
|
|
104
|
+
var record = {
|
|
111
105
|
type: "user_message",
|
|
112
106
|
text: text,
|
|
113
107
|
_internal: true,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
108
|
+
};
|
|
109
|
+
if (meta) {
|
|
110
|
+
var keys = Object.keys(meta);
|
|
111
|
+
for (var i = 0; i < keys.length; i++) record[keys[i]] = meta[keys[i]];
|
|
112
|
+
}
|
|
113
|
+
sm.sendAndRecord(caller, record);
|
|
117
114
|
caller.lastActivity = Date.now();
|
|
118
115
|
var sdk = ctx.getSdk();
|
|
119
116
|
if (!sdk) {
|
|
@@ -138,6 +135,33 @@ function attachSessionPair(ctx) {
|
|
|
138
135
|
return true;
|
|
139
136
|
}
|
|
140
137
|
|
|
138
|
+
function resumeDriverWithResult(caller, partner, token) {
|
|
139
|
+
if (!token.detached || token.delivered || caller.destroying) return false;
|
|
140
|
+
token.delivered = true;
|
|
141
|
+
var failure = token.failure || null;
|
|
142
|
+
var response = token.response || "";
|
|
143
|
+
var text;
|
|
144
|
+
if (token.interrupted) {
|
|
145
|
+
text = "[Split Worker execution interrupted] The user interrupted the Split 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.";
|
|
146
|
+
} else {
|
|
147
|
+
text = "A Split Worker task delegated through send_to_partner has finished.\n\n" +
|
|
148
|
+
"Original task:\n" + token.message + "\n\n" +
|
|
149
|
+
(failure ? "Split Worker error:\n" + failure : "Split Worker result:\n" + (response || "(No text response was recorded.)")) +
|
|
150
|
+
"\n\nReview the result, verify it as needed, and continue the task.";
|
|
151
|
+
}
|
|
152
|
+
return resumeDriverWithMessage(caller, text, {
|
|
153
|
+
partnerResult: true,
|
|
154
|
+
partnerSessionId: partner.localId,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function requestDetach(worker) {
|
|
159
|
+
var token = worker && worker._pairDelegation;
|
|
160
|
+
if (!token || token.detached || token.detachRequested) return false;
|
|
161
|
+
token.detachRequested = true;
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
141
165
|
function handleTurnDone(partner) {
|
|
142
166
|
var token = partner && partner._pairDelegation;
|
|
143
167
|
if (!token) return false;
|
|
@@ -179,7 +203,8 @@ function attachSessionPair(ctx) {
|
|
|
179
203
|
});
|
|
180
204
|
return;
|
|
181
205
|
}
|
|
182
|
-
|
|
206
|
+
// Timeouts and permission requests both detach into the push-back path.
|
|
207
|
+
if (Date.now() >= deadline || token.detachRequested) {
|
|
183
208
|
clearInterval(timer);
|
|
184
209
|
token.detached = true;
|
|
185
210
|
monitorPartner(group, caller, partner, token);
|
|
@@ -203,13 +228,11 @@ function attachSessionPair(ctx) {
|
|
|
203
228
|
async function sendToPartner(args, caller) {
|
|
204
229
|
try {
|
|
205
230
|
if (caller && caller._delegatedBy) throw new Error("delegated turns cannot delegate to another session");
|
|
231
|
+
var resolved = groupAndPartner(caller,
|
|
232
|
+
"send_to_partner requires an exact existing Driver/Split Worker pair; call propose_worker while unpaired");
|
|
206
233
|
var message = typeof args.message === "string" ? args.message.trim() : "";
|
|
207
234
|
if (!message) throw new Error("message is required");
|
|
208
|
-
|
|
209
|
-
if (caller && !store.groupForMember(caller.localId)) {
|
|
210
|
-
created = createWorkerForDriver(caller, args);
|
|
211
|
-
}
|
|
212
|
-
var resolved = groupAndPartner(caller);
|
|
235
|
+
turnControl.assertWorkerAction(caller);
|
|
213
236
|
var wait = args.wait !== false;
|
|
214
237
|
var timeout = Number.isFinite(args.timeoutSeconds) ? Math.floor(args.timeoutSeconds) : 300;
|
|
215
238
|
timeout = Math.max(1, Math.min(900, timeout));
|
|
@@ -247,13 +270,18 @@ function attachSessionPair(ctx) {
|
|
|
247
270
|
if (!wait) {
|
|
248
271
|
token.detached = true;
|
|
249
272
|
monitorPartner(resolved.group, caller, partner, token);
|
|
250
|
-
return toolResult({ status: "running", partnerId: partner.localId,
|
|
273
|
+
return toolResult({ status: "running", partnerId: partner.localId, hint: "The completed result will be pushed back automatically. Use read_partner only for an interim status check." });
|
|
251
274
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
275
|
+
if (!partner.isProcessing && !partner._queryStarting) {
|
|
276
|
+
finishDelegation(resolved.group, caller, partner, token);
|
|
277
|
+
var immediateFailure = errorSince(partner.history || [], token.startIndex);
|
|
278
|
+
return toolResult({
|
|
279
|
+
status: immediateFailure ? "error" : "complete",
|
|
280
|
+
response: responseText(partner.history || [], token.startIndex),
|
|
281
|
+
error: immediateFailure || undefined,
|
|
282
|
+
});
|
|
256
283
|
}
|
|
284
|
+
var completed = await waitForPartner(resolved.group, caller, partner, token, timeout);
|
|
257
285
|
return toolResult(completed);
|
|
258
286
|
} catch (e) {
|
|
259
287
|
if (caller) {
|
|
@@ -274,13 +302,15 @@ function attachSessionPair(ctx) {
|
|
|
274
302
|
try {
|
|
275
303
|
var resolved = groupAndPartner(caller);
|
|
276
304
|
var count = Number.isFinite(args.lastTurns) ? Math.floor(args.lastTurns) : 1;
|
|
277
|
-
count = Math.max(
|
|
278
|
-
|
|
305
|
+
count = Math.max(0, Math.min(5, count));
|
|
306
|
+
var payload = {
|
|
279
307
|
status: resolved.partner.isProcessing ? "running" : (resolved.partner._lastTurnInterrupted ? "interrupted" : "idle"),
|
|
280
308
|
partnerId: resolved.partner.localId,
|
|
281
309
|
title: resolved.partner.title || "New Session",
|
|
282
|
-
turns: recentTurns(resolved.partner, count),
|
|
283
|
-
}
|
|
310
|
+
turns: count > 0 ? recentTurns(resolved.partner, count) : [],
|
|
311
|
+
};
|
|
312
|
+
payload.capacity = lifecycle.optionalStatus(caller);
|
|
313
|
+
return toolResult(payload);
|
|
284
314
|
} catch (e) {
|
|
285
315
|
return toolError(e);
|
|
286
316
|
}
|
|
@@ -311,6 +341,7 @@ function attachSessionPair(ctx) {
|
|
|
311
341
|
if (partner.abortController) partner.abortController.abort();
|
|
312
342
|
}
|
|
313
343
|
if (partner._pairDelegation) finishDelegation(resolved.group, caller, partner, partner._pairDelegation);
|
|
344
|
+
workerPermission.cancelForSession(partner, "The Driver closed the Split Worker pair.");
|
|
314
345
|
var ws = { _clayUser: caller.ownerId ? { id: caller.ownerId } : null };
|
|
315
346
|
var result = store.dissolve(ws, { id: resolved.group.id });
|
|
316
347
|
if (!result.ok) throw new Error(result.error || "could not close the Worker pair");
|
|
@@ -320,115 +351,85 @@ function attachSessionPair(ctx) {
|
|
|
320
351
|
}
|
|
321
352
|
}
|
|
322
353
|
|
|
354
|
+
function handleHumanStop(session) {
|
|
355
|
+
var roles = turnControl.markHumanStop(session);
|
|
356
|
+
if (!roles) return false;
|
|
357
|
+
workerPermission.cancelForSession(roles.worker, "The human stopped this Split Worker turn.");
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
|
|
323
361
|
function getToolDefs(boundSession) {
|
|
324
362
|
if (!boundSession) return pairMcp.getToolDefs({
|
|
325
363
|
send: function () { return toolError(new Error("send_to_partner requires a session-bound tool server")); },
|
|
326
364
|
read: function () { return toolError(new Error("read_partner requires a session-bound tool server")); },
|
|
327
365
|
interrupt: function () { return toolError(new Error("interrupt_partner requires a session-bound tool server")); },
|
|
328
366
|
close: function () { return toolError(new Error("close_partner requires a session-bound tool server")); },
|
|
367
|
+
status: function () { return toolError(new Error("partner_status requires a session-bound tool server")); },
|
|
368
|
+
replace: function () { return toolError(new Error("replace_partner requires a session-bound tool server")); },
|
|
369
|
+
evaluate: function () { return toolError(new Error("record_partner_evaluation requires a session-bound tool server")); },
|
|
329
370
|
});
|
|
330
|
-
// Mount whenever we have a bound session: MCP servers are fixed for the
|
|
331
|
-
// lifetime of a query, and Claude queries live across turns, so gating on
|
|
332
|
-
// group membership HERE would permanently hide the tools from a session
|
|
333
|
-
// whose split is created mid-conversation (the ad-hoc flow). Handlers
|
|
334
|
-
// re-resolve the group on every call, so an ungrouped session gets a
|
|
335
|
-
// clear error, and a session that gains a partner later just works.
|
|
336
|
-
// The one structural exclusion: a session that is ALREADY a configured
|
|
337
|
-
// pair worker at query start never sees the tools.
|
|
338
371
|
var group = store.groupForMember(boundSession.localId);
|
|
339
372
|
if (group && group.pair && group.pair.driverId !== boundSession.localId) return [];
|
|
373
|
+
var adHocSplit = !!(group && !group.pair);
|
|
374
|
+
if (!driverEligibility.isEligibleDriverSession(boundSession, sm)) return [];
|
|
375
|
+
var persistentCatalog = boundSession.vendor === "codex" && (!group || !!group.pair);
|
|
376
|
+
if (!group && !persistentCatalog) {
|
|
377
|
+
return workerProposal.getToolDefs(boundSession)
|
|
378
|
+
.concat(workerPermission.getToolDefs(boundSession, { dormantDriver: true }));
|
|
379
|
+
}
|
|
380
|
+
var lifecycleHandlers = lifecycle.toolHandlers(boundSession);
|
|
340
381
|
var tools = pairMcp.getToolDefs({
|
|
341
|
-
send: function (args) {
|
|
382
|
+
send: function (args) {
|
|
383
|
+
return turnControl.runOperation(boundSession, "send", args && args.operationId, function () {
|
|
384
|
+
return sendToPartner(args || {}, boundSession);
|
|
385
|
+
});
|
|
386
|
+
},
|
|
342
387
|
read: function (args) { return readPartner(args, boundSession); },
|
|
343
388
|
interrupt: function (args) { return interruptPartner(args, boundSession); },
|
|
344
389
|
close: function (args) { return closePartner(args, boundSession); },
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
var driverSpec = msg.driver || {};
|
|
358
|
-
var workerSpec = msg.worker || {};
|
|
359
|
-
var workerVendor = validateVendor(workerSpec.vendor || sm.lastVendor || "codex");
|
|
360
|
-
var ownerId = ws._clayUser && ctx.usersModule.isMultiUser() ? ws._clayUser.id : null;
|
|
361
|
-
var driver;
|
|
362
|
-
var groupName;
|
|
363
|
-
if (Number.isInteger(driverSpec.sessionId)) {
|
|
364
|
-
// "Add Split Worker" on an existing session: the current session becomes
|
|
365
|
-
// the Driver with its full conversation context intact.
|
|
366
|
-
driver = sm.sessions.get(driverSpec.sessionId);
|
|
367
|
-
if (!driver) throw new Error("driver session not found");
|
|
368
|
-
if ((driver.ownerId || null) !== ownerId) throw new Error("driver session access denied");
|
|
369
|
-
if (store.groupForMember(driver.localId)) throw new Error("this session is already in a split group");
|
|
370
|
-
groupName = undefined; // auto name from member titles
|
|
371
|
-
} else {
|
|
372
|
-
var driverVendor = validateVendor(driverSpec.vendor || "claude");
|
|
373
|
-
var driverEffort = yoke.clampEffort(driverVendor, driverSpec.effort || (sm.currentEffortByVendor && sm.currentEffortByVendor[driverVendor]) || sm.currentEffort || "medium") || null;
|
|
374
|
-
driver = sm.createSessionRaw({ ownerId: ownerId, vendor: driverVendor, model: driverSpec.model || null, effort: driverEffort });
|
|
375
|
-
driver.title = "Driver · " + ((yoke.getVendorInfo(driverVendor) || {}).displayName || driverVendor);
|
|
376
|
-
if (driverSpec.effort) driver.loopSettings = { effort: driverSpec.effort };
|
|
377
|
-
groupName = "Agent pair";
|
|
390
|
+
status: lifecycleHandlers.status,
|
|
391
|
+
replace: function (args) {
|
|
392
|
+
return turnControl.runOperation(boundSession, "replace", args && args.operationId, function () {
|
|
393
|
+
return workerProposal.proposeReplacement(args || {}, boundSession);
|
|
394
|
+
});
|
|
395
|
+
},
|
|
396
|
+
evaluate: lifecycleHandlers.evaluate,
|
|
397
|
+
}, { lifecycle: persistentCatalog || !adHocSplit });
|
|
398
|
+
if (persistentCatalog) {
|
|
399
|
+
return workerProposal.getToolDefs(boundSession, { persistent: true })
|
|
400
|
+
.concat(tools)
|
|
401
|
+
.concat(workerPermission.getToolDefs(boundSession, { dormantDriver: true }));
|
|
378
402
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
worker.title = "Split Worker · " + ((yoke.getVendorInfo(workerVendor) || {}).displayName || workerVendor);
|
|
382
|
-
if (workerSpec.effort) worker.loopSettings = { effort: workerSpec.effort };
|
|
383
|
-
var result = store.create(ws, {
|
|
384
|
-
members: [driver.localId, worker.localId],
|
|
385
|
-
pair: { driverId: driver.localId, workerId: worker.localId },
|
|
386
|
-
name: groupName,
|
|
387
|
-
});
|
|
388
|
-
if (!result.ok) throw new Error(result.error);
|
|
389
|
-
sm.broadcastSessionList();
|
|
390
|
-
return { driver: driver, worker: worker, group: result.group };
|
|
403
|
+
return tools
|
|
404
|
+
.concat(workerPermission.getToolDefs(boundSession, { dormantDriver: false }));
|
|
391
405
|
}
|
|
392
406
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (vendor) validateVendor(vendor);
|
|
399
|
-
if (!vendor) {
|
|
400
|
-
if (installed.indexOf("codex") !== -1 && driver.vendor !== "codex") vendor = "codex";
|
|
401
|
-
else {
|
|
402
|
-
vendor = installed[0];
|
|
403
|
-
for (var i = 0; i < installed.length; i++) {
|
|
404
|
-
if (installed[i] !== driver.vendor) { vendor = installed[i]; break; }
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
var ws = {
|
|
409
|
-
_clayActiveSession: driver.localId,
|
|
410
|
-
_clayUser: driver.ownerId ? { id: driver.ownerId } : null,
|
|
411
|
-
};
|
|
412
|
-
var created = createPairRecord(ws, {
|
|
413
|
-
driver: { sessionId: driver.localId },
|
|
414
|
-
worker: {
|
|
415
|
-
vendor: vendor,
|
|
416
|
-
model: typeof args.workerModel === "string" ? args.workerModel : "",
|
|
417
|
-
effort: typeof args.workerEffort === "string" ? args.workerEffort : "",
|
|
418
|
-
},
|
|
419
|
-
});
|
|
420
|
-
sm.sendToSession(driver, { type: "pair_session_created", ok: true, group: created.group });
|
|
421
|
-
return created;
|
|
422
|
-
}
|
|
407
|
+
var factory = attachPairFactory(ctx);
|
|
408
|
+
var createPairRecord = factory.createPairRecord;
|
|
409
|
+
var createWorkerForDriver = factory.createWorkerForDriver;
|
|
410
|
+
var createPair = factory.createPair;
|
|
411
|
+
var preflightWorkerForDriver = factory.preflightWorkerForDriver;
|
|
423
412
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
413
|
+
lifecycle = attachPairLifecycle({
|
|
414
|
+
sm: sm,
|
|
415
|
+
splitStore: store,
|
|
416
|
+
createWorkerForDriver: function (driver, args) { return createWorkerForDriver(driver, args); },
|
|
417
|
+
preflightWorkerForDriver: function (driver, args) { return preflightWorkerForDriver(driver, args); },
|
|
418
|
+
sendToPartner: function (args, caller) { return sendToPartner(args, caller); },
|
|
419
|
+
finishDelegation: finishDelegation,
|
|
420
|
+
cancelWorkerPermissions: function (worker, reason) {
|
|
421
|
+
return workerPermission.cancelForSession(worker, reason);
|
|
422
|
+
},
|
|
423
|
+
turnControl: turnControl,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
workerPermission = attachWorkerPermission({
|
|
427
|
+
sm: sm,
|
|
428
|
+
splitStore: store,
|
|
429
|
+
onProcessingChanged: ctx.onProcessingChanged,
|
|
430
|
+
resumeDriverWithMessage: resumeDriverWithMessage,
|
|
431
|
+
requestDetach: requestDetach,
|
|
432
|
+
});
|
|
432
433
|
|
|
433
434
|
workerProposal = attachWorkerProposal({
|
|
434
435
|
sm: sm,
|
|
@@ -440,8 +441,11 @@ function attachSessionPair(ctx) {
|
|
|
440
441
|
getLinuxUserForSession: ctx.getLinuxUserForSession,
|
|
441
442
|
onProcessingChanged: ctx.onProcessingChanged,
|
|
442
443
|
adapters: ctx.adapters,
|
|
444
|
+
dangerouslySkipPermissions: ctx.dangerouslySkipPermissions,
|
|
443
445
|
createPairRecord: createPairRecord,
|
|
446
|
+
recordGenerationStart: function (driver, worker) { return lifecycle.recordGenerationStart(driver, worker); },
|
|
444
447
|
sendToPartner: sendToPartner,
|
|
448
|
+
replacePartner: function (args, session) { return lifecycle.replacePartner(args, session); },
|
|
445
449
|
});
|
|
446
450
|
|
|
447
451
|
function handleMessage(ws, msg) {
|
|
@@ -468,18 +472,27 @@ function attachSessionPair(ctx) {
|
|
|
468
472
|
var group = store.groupForMember(session.localId);
|
|
469
473
|
var pairPrompt = "";
|
|
470
474
|
if (group && group.pair && group.pair.driverId === session.localId) {
|
|
471
|
-
pairPrompt =
|
|
472
|
-
|
|
473
|
-
|
|
475
|
+
pairPrompt = pairPrompts.DRIVER_CORE;
|
|
476
|
+
if (driverOrchestration.isHighTierDriverSession(session, sm)) {
|
|
477
|
+
pairPrompt += " " + pairPrompts.DRIVER_DELEGATION;
|
|
478
|
+
}
|
|
479
|
+
} else if (!group && !ctx.isMate && driverEligibility.isEligibleDriverSession(session, sm) &&
|
|
480
|
+
driverOrchestration.isHighTierDriverSession(session, sm)) {
|
|
481
|
+
pairPrompt = pairPrompts.UNPAIRED + " " + workerProposal.getSystemPrompt(session);
|
|
474
482
|
}
|
|
475
|
-
return
|
|
483
|
+
return pairPrompt;
|
|
476
484
|
}
|
|
477
485
|
|
|
478
|
-
return {
|
|
486
|
+
return {
|
|
487
|
+
beginHumanTurn: turnControl.beginHumanTurn,
|
|
488
|
+
getToolDefs: getToolDefs,
|
|
489
|
+
handleHumanStop: handleHumanStop,
|
|
490
|
+
handleMessage: handleMessage,
|
|
491
|
+
handleTurnDone: handleTurnDone,
|
|
492
|
+
getSystemPrompt: getSystemPrompt,
|
|
493
|
+
respondToWorkerProposal: workerProposal.respondToProposal,
|
|
494
|
+
workerPermission: workerPermission,
|
|
495
|
+
};
|
|
479
496
|
}
|
|
480
497
|
|
|
481
|
-
module.exports = {
|
|
482
|
-
attachSessionPair: attachSessionPair,
|
|
483
|
-
responseText: responseText,
|
|
484
|
-
recentTurns: recentTurns,
|
|
485
|
-
};
|
|
498
|
+
module.exports = { attachSessionPair: attachSessionPair, responseText: responseText, recentTurns: recentTurns };
|
|
@@ -198,7 +198,7 @@ function attachSessionSpawn(ctx) {
|
|
|
198
198
|
var create = typeof sm.createSessionRaw === "function" ? sm.createSessionRaw : sm.createSession;
|
|
199
199
|
var session = create.call(sm, {
|
|
200
200
|
ownerId: parent.ownerId || null,
|
|
201
|
-
sessionVisibility:
|
|
201
|
+
sessionVisibility: "private",
|
|
202
202
|
vendor: vendor,
|
|
203
203
|
});
|
|
204
204
|
session.spawn = { parentId: parent.localId, index: index, batchId: batchId };
|
package/lib/project-sessions.js
CHANGED
|
@@ -3,7 +3,9 @@ var path = require("path");
|
|
|
3
3
|
var crypto = require("crypto");
|
|
4
4
|
var { execFileSync } = require("child_process");
|
|
5
5
|
var { CODEX_DEFAULTS, getCodexConfig } = require("./codex-defaults");
|
|
6
|
+
var updateSnooze = require("./update-snooze");
|
|
6
7
|
var yoke = require("./yoke");
|
|
8
|
+
var sessionVisibility = require("./session-visibility");
|
|
7
9
|
|
|
8
10
|
function vendorSupportsTui(vendor) {
|
|
9
11
|
var info = yoke.getVendorInfo(vendor);
|
|
@@ -71,6 +73,9 @@ function attachSessions(ctx) {
|
|
|
71
73
|
var send = ctx.send;
|
|
72
74
|
var sendTo = ctx.sendTo;
|
|
73
75
|
var sendToAdmins = ctx.sendToAdmins;
|
|
76
|
+
var broadcastUpdateAvailable = ctx.broadcastUpdateAvailable || function () {};
|
|
77
|
+
var broadcastUpdateStarted = ctx.broadcastUpdateStarted || function () {};
|
|
78
|
+
var sendToUpdateUser = ctx.sendToUpdateUser || function () {};
|
|
74
79
|
var sendToSession = ctx.sendToSession;
|
|
75
80
|
var sendToSessionOthers = ctx.sendToSessionOthers;
|
|
76
81
|
var opts = ctx.opts;
|
|
@@ -492,7 +497,7 @@ function attachSessions(ctx) {
|
|
|
492
497
|
sm.sweepBlankSessions();
|
|
493
498
|
var sessionOpts = {};
|
|
494
499
|
if (ws._clayUser && usersModule.isMultiUser()) sessionOpts.ownerId = ws._clayUser.id;
|
|
495
|
-
|
|
500
|
+
sessionOpts.sessionVisibility = sessionVisibility.normalize(msg.sessionVisibility);
|
|
496
501
|
if (msg.vendor) sessionOpts.vendor = msg.vendor;
|
|
497
502
|
// Mode resolution: vendors without a TUI session mode are always GUI.
|
|
498
503
|
// TUI-capable sessions honor the explicit msg.mode if provided, otherwise
|
|
@@ -577,7 +582,8 @@ function attachSessions(ctx) {
|
|
|
577
582
|
if (reusable) {
|
|
578
583
|
if (sessionOpts.vendor && reusable.vendor !== sessionOpts.vendor) reusable.vendor = sessionOpts.vendor;
|
|
579
584
|
if (!reusable.effort) reusable.effort = sessionOpts.effort;
|
|
580
|
-
|
|
585
|
+
sm.setSessionVisibility(reusable.localId, sessionOpts.sessionVisibility);
|
|
586
|
+
sessionVisibility.revokeViewers(ctx, reusable);
|
|
581
587
|
sm.switchSession(reusable.localId, ws);
|
|
582
588
|
newSess = reusable;
|
|
583
589
|
} else {
|
|
@@ -605,9 +611,7 @@ function attachSessions(ctx) {
|
|
|
605
611
|
}
|
|
606
612
|
|
|
607
613
|
if (msg.type === "set_session_visibility") {
|
|
608
|
-
|
|
609
|
-
sm.setSessionVisibility(msg.sessionId, msg.visibility);
|
|
610
|
-
}
|
|
614
|
+
sessionVisibility.handleChange(ctx, ws, msg);
|
|
611
615
|
return true;
|
|
612
616
|
}
|
|
613
617
|
|
|
@@ -897,25 +901,30 @@ function attachSessions(ctx) {
|
|
|
897
901
|
}
|
|
898
902
|
|
|
899
903
|
if (msg.type === "set_update_channel") {
|
|
900
|
-
if (
|
|
904
|
+
if (!updateSnooze.isUpdateAdmin(ws, usersModule)) return true;
|
|
901
905
|
var newChannel = msg.channel === "beta" ? "beta" : "stable";
|
|
902
906
|
setUpdateChannel(newChannel);
|
|
903
907
|
setLatestVersion(null);
|
|
904
908
|
if (typeof opts.onSetUpdateChannel === "function") {
|
|
905
909
|
opts.onSetUpdateChannel(newChannel);
|
|
906
910
|
}
|
|
907
|
-
// Re-fetch with new channel and broadcast to admin clients
|
|
911
|
+
// Re-fetch with new channel and broadcast to admin clients, honouring
|
|
912
|
+
// any snooze each of them has on the version that comes back.
|
|
908
913
|
fetchVersion(newChannel).then(function (v) {
|
|
909
914
|
if (v && isNewer(v, currentVersion)) {
|
|
910
915
|
setLatestVersion(v);
|
|
911
|
-
|
|
916
|
+
broadcastUpdateAvailable(v);
|
|
912
917
|
}
|
|
913
918
|
}).catch(function () {});
|
|
914
919
|
return true;
|
|
915
920
|
}
|
|
916
921
|
|
|
917
922
|
if (msg.type === "check_update") {
|
|
918
|
-
if (
|
|
923
|
+
if (!updateSnooze.isUpdateAdmin(ws, usersModule)) return true;
|
|
924
|
+
// A manual check is the user deliberately re-engaging, so it answers
|
|
925
|
+
// truthfully and ends any snooze rather than reporting an update it
|
|
926
|
+
// would then refuse to show.
|
|
927
|
+
updateSnooze.clearSnooze(updateSnooze.userKeyFor(ws, usersModule));
|
|
919
928
|
var updateChannel = getUpdateChannel();
|
|
920
929
|
fetchVersion(updateChannel).then(function (v) {
|
|
921
930
|
if (v && isNewer(v, currentVersion)) {
|
|
@@ -928,9 +937,40 @@ function attachSessions(ctx) {
|
|
|
928
937
|
return true;
|
|
929
938
|
}
|
|
930
939
|
|
|
940
|
+
if (msg.type === "update_snooze") {
|
|
941
|
+
if (!updateSnooze.isUpdateAdmin(ws, usersModule)) return true;
|
|
942
|
+
var snoozeKey = updateSnooze.userKeyFor(ws, usersModule);
|
|
943
|
+
// The target is always the server's own latest version. A version named
|
|
944
|
+
// in the payload is ignored, as are any timestamps or user ids.
|
|
945
|
+
var snoozeResult = updateSnooze.snooze(
|
|
946
|
+
snoozeKey,
|
|
947
|
+
getLatestVersion(),
|
|
948
|
+
msg.duration,
|
|
949
|
+
Date.now(),
|
|
950
|
+
msg.tzOffsetMinutes
|
|
951
|
+
);
|
|
952
|
+
if (!snoozeResult.ok) {
|
|
953
|
+
sendTo(ws, { type: "update_snoozed", ok: false, error: snoozeResult.error });
|
|
954
|
+
return true;
|
|
955
|
+
}
|
|
956
|
+
// Every live connection of this user, so a second device stops showing
|
|
957
|
+
// a banner the user has already dealt with.
|
|
958
|
+
sendToUpdateUser(snoozeKey, {
|
|
959
|
+
type: "update_snoozed",
|
|
960
|
+
ok: true,
|
|
961
|
+
version: snoozeResult.version,
|
|
962
|
+
until: snoozeResult.until,
|
|
963
|
+
duration: snoozeResult.duration,
|
|
964
|
+
});
|
|
965
|
+
return true;
|
|
966
|
+
}
|
|
967
|
+
|
|
931
968
|
if (msg.type === "update_now") {
|
|
932
|
-
if (
|
|
933
|
-
|
|
969
|
+
if (!updateSnooze.isUpdateAdmin(ws, usersModule)) return true;
|
|
970
|
+
// Installing makes any snooze moot; drop it so the record does not
|
|
971
|
+
// outlive the version it referred to.
|
|
972
|
+
updateSnooze.clearSnooze(updateSnooze.userKeyFor(ws, usersModule));
|
|
973
|
+
broadcastUpdateStarted(getLatestVersion());
|
|
934
974
|
var _ipc = require("./ipc");
|
|
935
975
|
var _config = require("./config");
|
|
936
976
|
_ipc.sendIPCCommand(_config.socketPath(), { cmd: "update" });
|
|
@@ -964,7 +1004,8 @@ function attachSessions(ctx) {
|
|
|
964
1004
|
|
|
965
1005
|
if (msg.type === "stop") {
|
|
966
1006
|
var session = getSessionForWs(ws);
|
|
967
|
-
if (session && session.isProcessing) {
|
|
1007
|
+
if (session && (session.isProcessing || session._queryStarting)) {
|
|
1008
|
+
if (typeof ctx.onHumanPairStop === "function") ctx.onHumanPairStop(session);
|
|
968
1009
|
session.taskStopRequested = true;
|
|
969
1010
|
if (session.abortController) session.abortController.abort();
|
|
970
1011
|
}
|
|
@@ -1736,8 +1777,12 @@ function attachSessions(ctx) {
|
|
|
1736
1777
|
sendTo(ws, { type: "create_worktree_result", ok: false, error: "Invalid branch name" });
|
|
1737
1778
|
return true;
|
|
1738
1779
|
}
|
|
1780
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(wtDirName)) {
|
|
1781
|
+
sendTo(ws, { type: "create_worktree_result", ok: false, error: "Invalid worktree directory name" });
|
|
1782
|
+
return true;
|
|
1783
|
+
}
|
|
1739
1784
|
if (typeof onCreateWorktree === "function") {
|
|
1740
|
-
var wtResult = onCreateWorktree(slug, wtBranch, wtDirName, wtBase);
|
|
1785
|
+
var wtResult = onCreateWorktree(slug, wtBranch, wtDirName, wtBase, ws._clayUser);
|
|
1741
1786
|
sendTo(ws, { type: "create_worktree_result", ok: wtResult.ok, slug: wtResult.slug, error: wtResult.error });
|
|
1742
1787
|
} else {
|
|
1743
1788
|
sendTo(ws, { type: "create_worktree_result", ok: false, error: "Not supported" });
|