clay-server 4.3.0-beta.4 → 4.3.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/driver-continuation-pair.js +5 -1
- package/lib/multi-worker-feature.js +11 -0
- package/lib/project-connection.js +2 -0
- package/lib/project-pair-autonomous-stop.js +14 -8
- package/lib/project-pair-close-transaction.js +59 -0
- package/lib/project-pair-global-stop.js +18 -0
- package/lib/project-pair-lifecycle-status.js +34 -0
- package/lib/project-pair-lifecycle.js +58 -76
- package/lib/project-pair-message.js +1 -1
- package/lib/project-pair-owned-stop.js +7 -4
- package/lib/project-pair-result-capture.js +116 -0
- package/lib/project-pair-result-delivery.js +140 -0
- package/lib/project-pair-result-outbox.js +400 -0
- package/lib/project-pair-result-pipeline.js +16 -0
- package/lib/project-pair-result-recovery.js +229 -0
- package/lib/project-pair-structural-close.js +20 -0
- package/lib/project-pair-task-control.js +45 -18
- package/lib/project-pair-worker-close.js +68 -0
- package/lib/project-session-pair-prompt.js +26 -0
- package/lib/project-session-pair.js +87 -106
- package/lib/project-worker-creation-proposal.js +176 -0
- package/lib/project-worker-permission.js +7 -8
- package/lib/project-worker-proposal-runner.js +45 -0
- package/lib/project-worker-proposal.js +79 -115
- package/lib/project.js +38 -1
- package/lib/public/app.js +1 -0
- package/lib/public/css/messages.css +12 -0
- package/lib/public/css/pane.css +48 -2
- package/lib/public/modules/app-connection.js +49 -76
- package/lib/public/modules/app-messages.js +11 -0
- package/lib/public/modules/pair-result-status.js +87 -0
- package/lib/public/modules/project-activation.js +10 -7
- package/lib/public/modules/sidebar-sessions.js +31 -11
- package/lib/public/modules/split-group-helpers.js +74 -2
- package/lib/public/modules/split-pair-ui.js +28 -6
- package/lib/public/modules/split-pane-reconciler.js +28 -0
- package/lib/public/modules/split-pane-renderer.js +107 -0
- package/lib/public/modules/split-view.js +58 -188
- package/lib/public/modules/websocket-watchdog.js +126 -0
- package/lib/public/modules/worker-pane-lock.js +13 -3
- package/lib/server.js +1 -0
- package/lib/session-pair-factory.js +66 -0
- package/lib/session-pair-mcp-server.js +10 -5
- package/lib/session-pair-prompts.js +41 -25
- package/lib/session-pair-target.js +42 -0
- package/lib/session-pair-turn-control.js +11 -6
- package/lib/session-split-group-anchors.js +89 -7
- package/lib/session-split-group-persistence.js +136 -0
- package/lib/session-split-group-roles.js +164 -0
- package/lib/session-split-group-v2-store.js +146 -0
- package/lib/session-split-groups.js +109 -99
- package/lib/sessions.js +33 -2
- package/lib/ws-schema.js +2 -0
- package/package.json +1 -1
|
@@ -1,52 +1,38 @@
|
|
|
1
|
-
var pairMcp = require("./session-pair-mcp-server")
|
|
2
|
-
|
|
3
|
-
var
|
|
1
|
+
var pairMcp = require("./session-pair-mcp-server"); var { attachPairFactory } = require("./session-pair-factory"), { attachPairLifecycle } = require("./project-pair-lifecycle"), { attachPairTurnControl } = require("./session-pair-turn-control");
|
|
2
|
+
var driverEligibility = require("./session-driver-eligibility"), { attachWorkerPermission } = require("./project-worker-permission"), { attachWorkerProposal } = require("./project-worker-proposal");
|
|
3
|
+
var attachSessionPairPrompt = require("./project-session-pair-prompt").attachSessionPairPrompt;
|
|
4
|
+
var attachPairCloseTransaction = require("./project-pair-close-transaction").attachPairCloseTransaction;
|
|
4
5
|
var { attachPairMessage } = require("./project-pair-message"), { attachPairTaskControl } = require("./project-pair-task-control"), stopAutonomous = require("./project-pair-autonomous-stop").stopAutonomousWork, stopOwned = require("./project-pair-owned-stop").stopOwnedPair;
|
|
5
6
|
var pairHistory = require("./session-pair-history"), responseText = pairHistory.responseText, errorSince = pairHistory.errorSince, recentTurns = pairHistory.recentTurns, operationFingerprint = require("./session-pair-operation").fingerprint;
|
|
6
|
-
var beginPairQuery = require("./session-pair-query-start").beginPairQuery;
|
|
7
|
-
|
|
8
|
-
function toolError(err) { return Promise.resolve({ content: [{ type: "text", text: "Error: " + (err.message || err) }], isError: true }); }
|
|
7
|
+
var beginPairQuery = require("./session-pair-query-start").beginPairQuery, attachPairResultPipeline = require("./project-pair-result-pipeline").attachPairResultPipeline;
|
|
8
|
+
var pairTarget = require("./session-pair-target"), splitRoles = require("./session-split-group-roles"), stopAllWorkers = require("./project-pair-global-stop").stopAllWorkers;
|
|
9
|
+
function toolResult(value) { return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] }); } function toolError(err) { return Promise.resolve({ content: [{ type: "text", text: "Error: " + (err.message || err) }], isError: true }); }
|
|
9
10
|
function attachSessionPair(ctx) {
|
|
10
|
-
var sm = ctx.sm;
|
|
11
|
-
var
|
|
12
|
-
|
|
13
|
-
var workerPermission;
|
|
14
|
-
var lifecycle;
|
|
15
|
-
var taskControl;
|
|
16
|
-
var turnControl = attachPairTurnControl({ sm: sm, splitStore: store });
|
|
11
|
+
var sm = ctx.sm, store = ctx.splitStore, workerProposal, workerPermission, lifecycle, taskControl, closeTransaction;
|
|
12
|
+
var turnControl = attachPairTurnControl({ sm: sm, splitStore: store }), resultCapture;
|
|
13
|
+
function targetOptions(options) { return Object.assign({}, options || {}, { multiWorkerFeature: ctx.multiWorkerFeature }); }
|
|
17
14
|
function groupAndPartner(caller, missingPairMessage) {
|
|
15
|
+
var args = arguments[2];
|
|
16
|
+
var options = arguments[3];
|
|
18
17
|
if (!caller) throw new Error("partner tools require a session-bound tool server");
|
|
19
18
|
if (sm.sessions.get(caller.localId) !== caller) {
|
|
20
19
|
throw new Error("this session is no longer live; the partner tools are bound to an exact session");
|
|
21
20
|
}
|
|
22
21
|
var group = store.groupForMember(caller.localId);
|
|
23
22
|
if (!group) throw new Error(missingPairMessage || "this session is not in a split group");
|
|
24
|
-
|
|
25
|
-
throw new Error("only the configured Driver can direct this pair");
|
|
26
|
-
}
|
|
27
|
-
var partnerId = group.members[0] === caller.localId ? group.members[1] : group.members[0];
|
|
28
|
-
var partner = sm.sessions.get(partnerId);
|
|
29
|
-
if (!partner) throw new Error("split partner session was not found");
|
|
30
|
-
if (caller.ownerId !== partner.ownerId) throw new Error("split partner access denied");
|
|
31
|
-
return { group: group, partner: partner };
|
|
23
|
+
return pairTarget.resolveGroupTarget(sm, group, caller, args, targetOptions(options));
|
|
32
24
|
}
|
|
33
25
|
function rolesFor(session) {
|
|
34
|
-
|
|
35
|
-
var group = store.groupForMember(session.localId);
|
|
36
|
-
if (!group || !group.pair) return null;
|
|
37
|
-
var driver = sm.sessions.get(group.pair.driverId);
|
|
38
|
-
var worker = sm.sessions.get(group.pair.workerId);
|
|
39
|
-
if (!driver || !worker || driver.ownerId !== worker.ownerId) return null;
|
|
40
|
-
return { group: group, driver: driver, worker: worker };
|
|
26
|
+
return pairTarget.rolesFor(sm, store, session);
|
|
41
27
|
}
|
|
42
28
|
var pairMessage = attachPairMessage({
|
|
43
29
|
sm: sm,
|
|
44
30
|
getSdk: ctx.getSdk,
|
|
45
|
-
resolvePair: groupAndPartner,
|
|
31
|
+
resolvePair: function (caller, args) { return groupAndPartner(caller, null, args, { mutation: true }); },
|
|
46
32
|
turnControl: turnControl,
|
|
47
33
|
});
|
|
48
34
|
taskControl = attachPairTaskControl({
|
|
49
|
-
sm: sm, resolvePair: groupAndPartner, rolesFor: rolesFor, turnControl: turnControl,
|
|
35
|
+
sm: sm, resolvePair: function (caller, args, options) { return groupAndPartner(caller, null, args, options); }, rolesFor: rolesFor, turnControl: turnControl,
|
|
50
36
|
sendFollowup: function (args, caller) { return sendToPartner(args, caller); },
|
|
51
37
|
});
|
|
52
38
|
function broadcastDelegation(group, caller, partner, active) {
|
|
@@ -119,17 +105,21 @@ function attachSessionPair(ctx) {
|
|
|
119
105
|
function requestDetach(worker) {
|
|
120
106
|
var token = worker && worker._pairDelegation;
|
|
121
107
|
if (!token || token.detached || token.detachRequested) return false;
|
|
122
|
-
token.
|
|
108
|
+
if (token.outboxKey && ctx.resultOutbox && !ctx.resultOutbox.setRoute(token.outboxKey, "callback").ok) return false;
|
|
109
|
+
token.wait = false; token.detachRequested = true;
|
|
123
110
|
return true;
|
|
124
111
|
}
|
|
125
112
|
function handleTurnDone(partner) {
|
|
126
113
|
var token = partner && partner._pairDelegation;
|
|
127
114
|
if (!token) return false;
|
|
115
|
+
if (token._capturePending || token._captureComplete) return false;
|
|
116
|
+
if (!taskControl.matchesGeneration(partner, token)) return false;
|
|
128
117
|
var group = store.groupForMember(partner.localId);
|
|
129
118
|
if (!group || group.id !== token.groupId || group.members.indexOf(token.from) === -1) return false;
|
|
130
119
|
var caller = sm.sessions.get(token.from);
|
|
131
120
|
if (!caller || caller.ownerId !== partner.ownerId) return false;
|
|
132
|
-
|
|
121
|
+
var liveRoles = splitRoles.resolveLiveRoles(sm, group);
|
|
122
|
+
if (group.pair && (!liveRoles || liveRoles.driver !== caller || liveRoles.workers.indexOf(partner) === -1)) {
|
|
133
123
|
finishDelegation(group, caller, partner, token);
|
|
134
124
|
return false;
|
|
135
125
|
}
|
|
@@ -137,19 +127,16 @@ function attachSessionPair(ctx) {
|
|
|
137
127
|
token.failure = errorSince(partner.history || [], token.startIndex);
|
|
138
128
|
token.interrupted = !token.failure && !!partner._lastTurnInterrupted;
|
|
139
129
|
if (token.interrupted) taskControl.ensureSystemInterruption(partner, token);
|
|
140
|
-
taskControl.complete(partner, caller, token, token.failure ? "failed" : (token.interrupted ? "interrupted" : "completed"), token.response, token.failure);
|
|
141
|
-
|
|
142
|
-
if (typeof ctx.onPartnerResult === "function") ctx.onPartnerResult(caller, partner, token);
|
|
143
|
-
var resumed = resumeDriverWithResult(caller, partner, token);
|
|
144
|
-
taskControl.drain(caller, partner);
|
|
145
|
-
return resumed;
|
|
130
|
+
var outcome = taskControl.complete(partner, caller, token, token.failure ? "failed" : (token.interrupted ? "interrupted" : "completed"), token.response, token.failure);
|
|
131
|
+
return resultCapture.capture(caller, partner, token, outcome);
|
|
146
132
|
}
|
|
147
133
|
function waitForPartner(group, caller, partner, token, timeoutSeconds) {
|
|
148
134
|
return new Promise(function (resolve, reject) {
|
|
149
135
|
var deadline = Date.now() + timeoutSeconds * 1000;
|
|
150
136
|
var timer = setInterval(function () {
|
|
151
137
|
var currentGroup = store.groupForMember(caller.localId);
|
|
152
|
-
|
|
138
|
+
var exactTarget = splitRoles.matchesLiveTarget(sm, currentGroup, caller, partner);
|
|
139
|
+
if (!currentGroup || currentGroup.id !== group.id || !exactTarget) {
|
|
153
140
|
clearInterval(timer);
|
|
154
141
|
finishDelegation(group, caller, partner, token);
|
|
155
142
|
reject(new Error("the split group was dissolved while waiting for the partner"));
|
|
@@ -157,28 +144,34 @@ function attachSessionPair(ctx) {
|
|
|
157
144
|
}
|
|
158
145
|
if (!partner.isProcessing && !partner._queryStarting) {
|
|
159
146
|
clearInterval(timer);
|
|
160
|
-
|
|
147
|
+
if (!taskControl.matchesGeneration(partner, token)) return reject(new Error("the delegated task no longer matches this Worker generation"));
|
|
161
148
|
var failure = errorSince(partner.history || [], token.startIndex);
|
|
162
149
|
var interrupted = !failure && !!partner._lastTurnInterrupted;
|
|
163
150
|
var response = responseText(partner.history || [], token.startIndex);
|
|
164
151
|
if (interrupted) taskControl.ensureSystemInterruption(partner, token);
|
|
165
152
|
var outcome = taskControl.complete(partner, caller, token, failure ? "failed" : (interrupted ? "interrupted" : "completed"), response, failure);
|
|
153
|
+
resultCapture.capture(caller, partner, token, outcome);
|
|
166
154
|
resolve({
|
|
167
|
-
status: failure ? "error" : (outcome.status === "completed" ? "complete" : outcome.status),
|
|
155
|
+
status: token._capturePending ? "capture_pending" : (failure ? "error" : (outcome.status === "completed" ? "complete" : outcome.status)),
|
|
168
156
|
taskId: token.taskId,
|
|
169
157
|
generation: token.generation,
|
|
170
158
|
response: response,
|
|
171
159
|
error: failure || undefined,
|
|
172
160
|
outcome: outcome,
|
|
173
161
|
});
|
|
174
|
-
taskControl.drain(caller, partner);
|
|
175
162
|
return;
|
|
176
163
|
}
|
|
177
164
|
if (Date.now() >= deadline || token.detachRequested) {
|
|
178
165
|
clearInterval(timer);
|
|
179
|
-
token.
|
|
166
|
+
var routed = token.outboxKey && ctx.resultOutbox ? ctx.resultOutbox.setRoute(token.outboxKey, "callback") : { ok: true };
|
|
167
|
+
if (!routed.ok) {
|
|
168
|
+
monitorPartner(group, caller, partner, token);
|
|
169
|
+
resolve({ status: "capture_pending", taskId: token.taskId, generation: token.generation, response: responseText(partner.history || [], token.startIndex), error: routed.error });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
token.wait = false; token.detached = true;
|
|
180
173
|
monitorPartner(group, caller, partner, token);
|
|
181
|
-
resolve({ status: "running", taskId: token.taskId, generation: token.generation, response: responseText(partner.history || [], token.startIndex), hint: "The completed result will be pushed back automatically. Use read_partner only for
|
|
174
|
+
resolve({ status: "running", taskId: token.taskId, generation: token.generation, response: responseText(partner.history || [], token.startIndex), hint: "The completed result will be pushed back automatically. Use read_partner only for a requested status, concrete failure or stall diagnosis, or capacity/queue decision; do not poll routinely." });
|
|
182
175
|
}
|
|
183
176
|
}, 500);
|
|
184
177
|
});
|
|
@@ -186,29 +179,37 @@ function attachSessionPair(ctx) {
|
|
|
186
179
|
function monitorPartner(group, caller, partner, token) {
|
|
187
180
|
var timer = setInterval(function () {
|
|
188
181
|
var currentGroup = store.groupForMember(caller.localId);
|
|
189
|
-
|
|
182
|
+
var exactTarget = splitRoles.matchesLiveTarget(sm, currentGroup, caller, partner);
|
|
183
|
+
if (!currentGroup || currentGroup.id !== group.id || !exactTarget || (!partner.isProcessing && !partner._queryStarting)) {
|
|
190
184
|
clearInterval(timer);
|
|
191
|
-
if (currentGroup && currentGroup.id === group.id) handleTurnDone(partner);
|
|
185
|
+
if (currentGroup && currentGroup.id === group.id && exactTarget) handleTurnDone(partner);
|
|
192
186
|
else finishDelegation(group, caller, partner, token);
|
|
193
187
|
}
|
|
194
188
|
}, 500);
|
|
189
|
+
if (timer && typeof timer.unref === "function") timer.unref();
|
|
195
190
|
}
|
|
196
191
|
async function sendToPartner(args, caller) {
|
|
197
192
|
try {
|
|
198
193
|
if (caller && caller._delegatedBy) throw new Error("delegated turns cannot delegate to another session");
|
|
199
194
|
var resolved = groupAndPartner(caller,
|
|
200
|
-
"send_to_partner requires an exact existing Driver/Split Worker pair; call propose_worker while unpaired");
|
|
195
|
+
"send_to_partner requires an exact existing Driver/Split Worker pair; call propose_worker while unpaired", args, { mutation: true });
|
|
201
196
|
var message = typeof args.message === "string" ? args.message.trim() : "";
|
|
202
197
|
if (!message) throw new Error("message is required");
|
|
203
198
|
turnControl.assertWorkerAction(caller);
|
|
204
|
-
var wait = args.wait
|
|
199
|
+
var wait = args.wait === true;
|
|
205
200
|
var timeout = Number.isFinite(args.timeoutSeconds) ? Math.floor(args.timeoutSeconds) : 300;
|
|
206
201
|
timeout = Math.max(1, Math.min(900, timeout));
|
|
207
202
|
var partner = resolved.partner;
|
|
208
203
|
if (partner._pairDelegation) throw new Error("the partner is already handling a delegated task");
|
|
209
204
|
delete partner._pairInterruption; delete partner._lastInterruptedPairTask;
|
|
210
205
|
var task = taskControl.begin(partner, caller, message, args.taskId);
|
|
211
|
-
var token = Object.assign(task, { from: caller.localId, groupId: resolved.group.id, startIndex: partner.history.length });
|
|
206
|
+
var token = Object.assign(task, { from: caller.localId, groupId: resolved.group.id, startIndex: partner.history.length, wait: wait });
|
|
207
|
+
var started = resultCapture.begin(caller, partner, token, message, ctx.projectSlug);
|
|
208
|
+
if (!started.ok) {
|
|
209
|
+
token._retainOnError = true; partner._pairDelegation = token; partner._delegatedBy = caller.localId;
|
|
210
|
+
sm.sendAndRecord(caller, { type: "error", text: "The Split Worker task was not dispatched because its durable completion record could not be created: " + started.error });
|
|
211
|
+
return toolError(new Error(started.error));
|
|
212
|
+
}
|
|
212
213
|
partner._pairDelegation = token;
|
|
213
214
|
partner._delegatedBy = caller.localId;
|
|
214
215
|
sm.sendAndRecord(partner, {
|
|
@@ -243,14 +244,15 @@ function attachSessionPair(ctx) {
|
|
|
243
244
|
if (!wait) {
|
|
244
245
|
token.detached = true;
|
|
245
246
|
monitorPartner(resolved.group, caller, partner, token);
|
|
246
|
-
return toolResult({ status: "running", partnerId: partner.localId, taskId: token.taskId, generation: token.generation, hint: "The completed result will be pushed back automatically. Use read_partner only for
|
|
247
|
+
return toolResult({ status: "running", partnerId: partner.localId, taskId: token.taskId, generation: token.generation, hint: "The completed result will be pushed back automatically. Use read_partner only for a requested status, concrete failure or stall diagnosis, or capacity/queue decision; do not poll routinely." });
|
|
247
248
|
}
|
|
248
249
|
if (!partner.isProcessing && !partner._queryStarting) {
|
|
249
|
-
|
|
250
|
+
if (!taskControl.matchesGeneration(partner, token)) return toolError(new Error("the delegated task no longer matches this Worker generation"));
|
|
250
251
|
var immediateFailure = errorSince(partner.history || [], token.startIndex);
|
|
251
252
|
var immediateResponse = responseText(partner.history || [], token.startIndex);
|
|
252
253
|
var immediateOutcome = taskControl.complete(partner, caller, token, immediateFailure ? "failed" : "completed", immediateResponse, immediateFailure);
|
|
253
|
-
|
|
254
|
+
var immediateCapture = resultCapture.capture(caller, partner, token, immediateOutcome);
|
|
255
|
+
if (!immediateCapture && token._capturePending) return toolError(new Error("The completed Split Worker result could not be durably captured yet."));
|
|
254
256
|
return toolResult({
|
|
255
257
|
status: immediateFailure ? "error" : (immediateOutcome.status === "completed" ? "complete" : immediateOutcome.status),
|
|
256
258
|
taskId: token.taskId,
|
|
@@ -266,9 +268,10 @@ function attachSessionPair(ctx) {
|
|
|
266
268
|
if (caller) {
|
|
267
269
|
var group = store.groupForMember(caller.localId);
|
|
268
270
|
if (group) {
|
|
269
|
-
var
|
|
270
|
-
var
|
|
271
|
-
|
|
271
|
+
var failedRoles = splitRoles.resolveLiveRoles(sm, group);
|
|
272
|
+
var failedTarget = failedRoles && splitRoles.resolveWorkerTarget(failedRoles.role, args && args.workerId);
|
|
273
|
+
var partner = failedTarget && failedTarget.ok ? sm.sessions.get(failedTarget.workerId) : null;
|
|
274
|
+
if (partner && partner._pairDelegation && partner._pairDelegation.from === caller.localId && !partner.isProcessing && !partner._pairDelegation._retainOnError) {
|
|
272
275
|
finishDelegation(group, caller, partner, partner._pairDelegation);
|
|
273
276
|
}
|
|
274
277
|
}
|
|
@@ -278,7 +281,7 @@ function attachSessionPair(ctx) {
|
|
|
278
281
|
}
|
|
279
282
|
function readPartner(args, caller) {
|
|
280
283
|
try {
|
|
281
|
-
var resolved = groupAndPartner(caller);
|
|
284
|
+
var resolved = groupAndPartner(caller, null, args, { readOnly: true });
|
|
282
285
|
var count = Number.isFinite(args.lastTurns) ? Math.floor(args.lastTurns) : 1;
|
|
283
286
|
count = Math.max(0, Math.min(5, count));
|
|
284
287
|
var payload = {
|
|
@@ -287,7 +290,7 @@ function attachSessionPair(ctx) {
|
|
|
287
290
|
title: resolved.partner.title || "New Session",
|
|
288
291
|
turns: count > 0 ? recentTurns(resolved.partner, count) : [],
|
|
289
292
|
};
|
|
290
|
-
payload.capacity = lifecycle.optionalStatus(caller);
|
|
293
|
+
payload.capacity = lifecycle.optionalStatus(caller, args);
|
|
291
294
|
return toolResult(payload);
|
|
292
295
|
} catch (e) {
|
|
293
296
|
return toolError(e);
|
|
@@ -295,7 +298,7 @@ function attachSessionPair(ctx) {
|
|
|
295
298
|
}
|
|
296
299
|
function interruptPartner(args, caller) {
|
|
297
300
|
try {
|
|
298
|
-
var resolved = groupAndPartner(caller);
|
|
301
|
+
var resolved = groupAndPartner(caller, null, args, { mutation: true });
|
|
299
302
|
var partner = resolved.partner;
|
|
300
303
|
if (!partner.isProcessing && !partner._queryStarting) {
|
|
301
304
|
return toolResult({ status: "idle", partnerId: partner.localId, title: partner.title || "New Session" });
|
|
@@ -308,42 +311,26 @@ function attachSessionPair(ctx) {
|
|
|
308
311
|
return toolError(e);
|
|
309
312
|
}
|
|
310
313
|
}
|
|
314
|
+
function closeWorker(args, caller) {
|
|
315
|
+
return closeTransaction.close(args, caller);
|
|
316
|
+
}
|
|
311
317
|
function closePartner(args, caller) {
|
|
312
318
|
try {
|
|
313
|
-
|
|
314
|
-
var partner = resolved.partner;
|
|
315
|
-
var interrupted = !!(partner.isProcessing || partner._queryStarting);
|
|
316
|
-
if (interrupted) {
|
|
317
|
-
taskControl.markInterruption(partner, "driver", "The Driver closed the Split Worker pair.");
|
|
318
|
-
partner.taskStopRequested = true;
|
|
319
|
-
if (partner.abortController) partner.abortController.abort();
|
|
320
|
-
}
|
|
321
|
-
if (partner._pairDelegation) {
|
|
322
|
-
completeInterruptedTask(partner, caller, partner._pairDelegation);
|
|
323
|
-
finishDelegation(resolved.group, caller, partner, partner._pairDelegation);
|
|
324
|
-
}
|
|
325
|
-
workerPermission.cancelForSession(partner, "The Driver closed the Split Worker pair.");
|
|
326
|
-
var ws = { _clayUser: caller.ownerId ? { id: caller.ownerId } : null };
|
|
327
|
-
var result = store.dissolve(ws, { id: resolved.group.id });
|
|
328
|
-
if (!result.ok) throw new Error(result.error || "could not close the Worker pair");
|
|
329
|
-
return toolResult({ status: "closed", partnerId: partner.localId, interrupted: interrupted, historyPreserved: true });
|
|
319
|
+
return toolResult(closeWorker(args, caller));
|
|
330
320
|
} catch (e) {
|
|
331
321
|
return toolError(e);
|
|
332
322
|
}
|
|
333
323
|
}
|
|
334
324
|
function handleHumanStop(session) {
|
|
335
325
|
var roles = turnControl.markHumanStop(session);
|
|
336
|
-
|
|
337
|
-
taskControl.markInterruption(roles.worker, "user", "The human stopped this Split Worker turn.");
|
|
338
|
-
workerPermission.cancelForSession(roles.worker, "The human stopped this Split Worker turn.");
|
|
339
|
-
return true;
|
|
326
|
+
return stopAllWorkers(session, roles, { resultOutbox: ctx.resultOutbox, taskControl: taskControl, workerPermission: workerPermission });
|
|
340
327
|
}
|
|
341
328
|
function stopOwnedPair(session, reason) { return stopOwned(session, rolesFor(session), turnControl, taskControl, workerPermission, reason); }
|
|
342
329
|
function stopAutonomousWork(session) { return stopAutonomous(session, rolesFor(session), turnControl, taskControl, workerPermission); }
|
|
343
330
|
function beginHumanTurn(session) {
|
|
344
331
|
var resumed = turnControl.beginHumanTurn(session);
|
|
345
332
|
var roles = resumed && rolesFor(session);
|
|
346
|
-
if (roles) taskControl.drain(roles.driver, roles.
|
|
333
|
+
if (roles) for (var i = 0; i < roles.workers.length; i++) taskControl.drain(roles.driver, roles.workers[i]);
|
|
347
334
|
return resumed;
|
|
348
335
|
}
|
|
349
336
|
function stopBarrier(session) { return turnControl.blockedReason(session); }
|
|
@@ -359,7 +346,8 @@ function attachSessionPair(ctx) {
|
|
|
359
346
|
evaluate: function () { return toolError(new Error("record_partner_evaluation requires a session-bound tool server")); },
|
|
360
347
|
});
|
|
361
348
|
var group = store.groupForMember(boundSession.localId);
|
|
362
|
-
|
|
349
|
+
var boundRoles = group && group.pair ? splitRoles.resolveLiveRoles(sm, group) : null;
|
|
350
|
+
if (boundRoles && boundRoles.workers.indexOf(boundSession) !== -1) return taskControl.workerToolDefs(boundSession);
|
|
363
351
|
var adHocSplit = !!(group && !group.pair);
|
|
364
352
|
if (!driverEligibility.isEligibleDriverSession(boundSession, sm)) return [];
|
|
365
353
|
var persistentCatalog = boundSession.vendor === "codex" && (!group || !!group.pair);
|
|
@@ -372,7 +360,7 @@ function attachSessionPair(ctx) {
|
|
|
372
360
|
send: function (args) {
|
|
373
361
|
return turnControl.runOperation(boundSession, "send", args && args.operationId, function () {
|
|
374
362
|
return sendToPartner(args || {}, boundSession);
|
|
375
|
-
}, operationFingerprint(args, ["message", "wait", "timeoutSeconds", "taskId"])).catch(toolError);
|
|
363
|
+
}, operationFingerprint(args, ["message", "wait", "timeoutSeconds", "taskId", "workerId"])).catch(toolError);
|
|
376
364
|
},
|
|
377
365
|
read: function (args) { return readPartner(args, boundSession); },
|
|
378
366
|
message: function (args) {
|
|
@@ -388,7 +376,7 @@ function attachSessionPair(ctx) {
|
|
|
388
376
|
replace: function (args) {
|
|
389
377
|
return turnControl.runOperation(boundSession, "replace", args && args.operationId, function () {
|
|
390
378
|
return workerProposal.proposeReplacement(args || {}, boundSession);
|
|
391
|
-
}, operationFingerprint(args, ["interrupt", "message", "workerVendor", "workerModel", "workerEffort", "recommendationRationale", "evaluation", "supersedeProposalId"])).catch(toolError);
|
|
379
|
+
}, operationFingerprint(args, ["interrupt", "message", "workerVendor", "workerModel", "workerEffort", "recommendationRationale", "evaluation", "supersedeProposalId", "workerId"])).catch(toolError);
|
|
392
380
|
},
|
|
393
381
|
evaluate: lifecycleHandlers.evaluate,
|
|
394
382
|
}, { lifecycle: persistentCatalog || !adHocSplit });
|
|
@@ -409,12 +397,14 @@ function attachSessionPair(ctx) {
|
|
|
409
397
|
}
|
|
410
398
|
var factory = attachPairFactory(ctx), createPairRecord = factory.createPairRecord,
|
|
411
399
|
createWorkerForDriver = factory.createWorkerForDriver, createPair = factory.createPair,
|
|
400
|
+
addWorkerForDriver = factory.addWorkerForDriver, replaceWorkerForDriver = factory.replaceWorkerForDriver,
|
|
412
401
|
preflightWorkerForDriver = factory.preflightWorkerForDriver, createOwnedPair = factory.createOwnedPair, removeOwnedPair = factory.removeOwnedPair,
|
|
413
402
|
preflightRuntime = factory.preflightRuntime;
|
|
414
403
|
lifecycle = attachPairLifecycle({
|
|
415
404
|
sm: sm,
|
|
416
405
|
splitStore: store,
|
|
417
406
|
createWorkerForDriver: function (driver, args) { return createWorkerForDriver(driver, args); },
|
|
407
|
+
replaceWorkerForDriver: function (driver, worker, args) { return replaceWorkerForDriver(driver, worker, args); },
|
|
418
408
|
preflightWorkerForDriver: function (driver, args) { return preflightWorkerForDriver(driver, args); },
|
|
419
409
|
sendToPartner: function (args, caller) { return sendToPartner(args, caller); },
|
|
420
410
|
finishDelegation: finishDelegation,
|
|
@@ -424,6 +414,7 @@ function attachSessionPair(ctx) {
|
|
|
424
414
|
taskStatus: function (worker) { return taskControl.status(worker); },
|
|
425
415
|
proposalStatus: function (driver) { return workerProposal ? workerProposal.proposalStatus(driver) : null; },
|
|
426
416
|
turnControl: turnControl,
|
|
417
|
+
multiWorkerFeature: ctx.multiWorkerFeature,
|
|
427
418
|
});
|
|
428
419
|
workerPermission = attachWorkerPermission({
|
|
429
420
|
sm: sm,
|
|
@@ -447,7 +438,9 @@ function attachSessionPair(ctx) {
|
|
|
447
438
|
recordGenerationStart: function (driver, worker) { return lifecycle.recordGenerationStart(driver, worker); },
|
|
448
439
|
sendToPartner: sendToPartner,
|
|
449
440
|
replacePartner: function (args, session) { return lifecycle.replacePartner(args, session); },
|
|
441
|
+
addWorkerForDriver: function (driver, args) { return addWorkerForDriver(driver, args); },
|
|
450
442
|
preflightWorkerForDriver: preflightWorkerForDriver,
|
|
443
|
+
multiWorkerFeature: ctx.multiWorkerFeature,
|
|
451
444
|
});
|
|
452
445
|
function handleMessage(ws, msg) {
|
|
453
446
|
if (workerProposal.handleMessage(ws, msg)) return true;
|
|
@@ -468,27 +461,15 @@ function attachSessionPair(ctx) {
|
|
|
468
461
|
}
|
|
469
462
|
return false;
|
|
470
463
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
pairPrompt += " " + pairPrompts.DRIVER_DELEGATION;
|
|
483
|
-
}
|
|
484
|
-
} else if (!group && !ctx.isMate && driverEligibility.isEligibleDriverSession(session, sm) &&
|
|
485
|
-
driverOrchestration.isHighTierDriverSession(session, sm)) {
|
|
486
|
-
pairPrompt = pairPrompts.UNPAIRED + " " + workerProposal.getSystemPrompt(session);
|
|
487
|
-
}
|
|
488
|
-
return pairPrompt;
|
|
489
|
-
}
|
|
490
|
-
return { beginHumanTurn: beginHumanTurn, getToolDefs: getToolDefs, handleHumanStop: handleHumanStop, stopOwnedPair: stopOwnedPair, stopAutonomousWork: stopAutonomousWork, stopBarrier: stopBarrier,
|
|
491
|
-
handleMessage: handleMessage, handleTurnDone: handleTurnDone, getSystemPrompt: getSystemPrompt,
|
|
492
|
-
respondToWorkerProposal: workerProposal.respondToProposal, workerPermission: workerPermission, createOwnedPair: createOwnedPair, removeOwnedPair: removeOwnedPair, preflightRuntime: preflightRuntime,
|
|
493
|
-
sendToPartner: sendToPartner, rolesFor: rolesFor };
|
|
464
|
+
var pairPrompt = attachSessionPairPrompt({ sm: sm, store: store, isMate: ctx.isMate, rolesFor: rolesFor,
|
|
465
|
+
workerProposal: function () { return workerProposal; } });
|
|
466
|
+
var resultPipeline = attachPairResultPipeline({ sm: sm, store: store, resultOutbox: ctx.resultOutbox, getSdk: ctx.getSdk, getLinuxUserForSession: ctx.getLinuxUserForSession, acceptanceTimeoutMs: ctx.acceptanceTimeoutMs, finishDelegation: finishDelegation, resumeDriverWithResult: resumeDriverWithResult, onPartnerResult: ctx.onPartnerResult, onStateChange: ctx.onResultStateChange, blockedReason: turnControl.blockedReason, drain: function (caller, partner) { return taskControl.drain(caller, partner); } });
|
|
467
|
+
resultCapture = resultPipeline.capture; var resultDelivery = resultPipeline.delivery;
|
|
468
|
+
closeTransaction = attachPairCloseTransaction({ store: store, multiWorkerFeature: ctx.multiWorkerFeature,
|
|
469
|
+
resolvePair: function (caller, args) { return groupAndPartner(caller, null, args, { mutation: true, structural: true }); },
|
|
470
|
+
taskControl: taskControl, resultCapture: resultCapture, workerPermission: workerPermission,
|
|
471
|
+
responseText: responseText, errorSince: errorSince });
|
|
472
|
+
return { beginHumanTurn: beginHumanTurn, getToolDefs: getToolDefs, handleHumanStop: handleHumanStop, stopOwnedPair: stopOwnedPair, stopAutonomousWork: stopAutonomousWork, stopBarrier: stopBarrier, handleMessage: handleMessage, handleTurnDone: handleTurnDone, getSystemPrompt: pairPrompt.getSystemPrompt,
|
|
473
|
+
respondToWorkerProposal: workerProposal.respondToProposal, workerPermission: workerPermission, addWorkerForDriver: addWorkerForDriver, createOwnedPair: createOwnedPair, removeOwnedPair: removeOwnedPair, preflightRuntime: preflightRuntime,
|
|
474
|
+
sendToPartner: sendToPartner, closeWorker: closeWorker, rolesFor: rolesFor, resultCapture: resultCapture, resultOutbox: ctx.resultOutbox, wakePartnerResult: function (caller, partner, token) { return resultDelivery.wake(caller, partner, token); }, getResultOutboxStatus: function () { return ctx.resultOutbox ? ctx.resultOutbox.status() : { records: [], persistenceError: null }; } };
|
|
494
475
|
} module.exports = { attachSessionPair: attachSessionPair, responseText: responseText, recentTurns: recentTurns };
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
var crypto = require("crypto");
|
|
2
|
+
var driverEligibility = require("./session-driver-eligibility");
|
|
3
|
+
var multiWorkerFeature = require("./multi-worker-feature");
|
|
4
|
+
var proposalControl = require("./worker-proposal-control");
|
|
5
|
+
var splitRoles = require("./session-split-group-roles");
|
|
6
|
+
|
|
7
|
+
function generationOf(session) {
|
|
8
|
+
return session && (session._pairGeneration ||
|
|
9
|
+
session.sessionProvenance && session.sessionProvenance.generation) || null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function captureMembership(sm, driver, group) {
|
|
13
|
+
var roles = splitRoles.resolveLiveRoles(sm, group);
|
|
14
|
+
if (!roles || roles.driver !== driver || roles.role.kind === "adhoc" || !driver.sessionOriginId) return null;
|
|
15
|
+
var workers = [];
|
|
16
|
+
for (var i = 0; i < roles.workers.length; i++) {
|
|
17
|
+
if (!roles.workers[i].sessionOriginId || !Number.isInteger(generationOf(roles.workers[i]))) return null;
|
|
18
|
+
workers.push({ id: roles.workers[i].localId, originId: roles.workers[i].sessionOriginId,
|
|
19
|
+
generation: generationOf(roles.workers[i]) });
|
|
20
|
+
}
|
|
21
|
+
return { groupId: group.id, ownerId: driver.ownerId || null, driverId: driver.localId,
|
|
22
|
+
driverOriginId: driver.sessionOriginId, workerIds: roles.role.workerIds.slice(), workers: workers };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function matchesMembership(sm, driver, group, snapshot) {
|
|
26
|
+
if (!snapshot || !driver || !group || group.id !== snapshot.groupId || driver.localId !== snapshot.driverId ||
|
|
27
|
+
(driver.ownerId || null) !== snapshot.ownerId || driver.sessionOriginId !== snapshot.driverOriginId) return false;
|
|
28
|
+
var current = captureMembership(sm, driver, group);
|
|
29
|
+
if (!current || current.workerIds.join(",") !== snapshot.workerIds.join(",")) return false;
|
|
30
|
+
for (var i = 0; i < current.workers.length; i++) {
|
|
31
|
+
var expected = snapshot.workers[i];
|
|
32
|
+
if (!expected || current.workers[i].id !== expected.id || current.workers[i].originId !== expected.originId ||
|
|
33
|
+
current.workers[i].generation !== expected.generation) return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function attachWorkerCreationProposal(ctx) {
|
|
39
|
+
var sm = ctx.sm;
|
|
40
|
+
var store = ctx.splitStore;
|
|
41
|
+
|
|
42
|
+
function initialSnapshot(session) {
|
|
43
|
+
if (!ctx.canOffer(session)) return { error: "Split Worker proposals require an eligible Driver session." };
|
|
44
|
+
var group = store.groupForMember(session.localId);
|
|
45
|
+
if (!group) return { kind: "initial", driver: session };
|
|
46
|
+
if (!multiWorkerFeature.isEnabled(ctx.multiWorkerFeature)) {
|
|
47
|
+
return { error: "This Driver already has a Split Worker." };
|
|
48
|
+
}
|
|
49
|
+
var roles = splitRoles.resolveLiveRoles(sm, group);
|
|
50
|
+
if (!roles || roles.driver !== session || roles.role.kind === "adhoc") {
|
|
51
|
+
return { error: "The exact Driver/Split Worker membership is not eligible for another Worker." };
|
|
52
|
+
}
|
|
53
|
+
if (roles.workers.length >= 2) return { error: "A Driver can have at most two Split Workers." };
|
|
54
|
+
if (roles.workers.length !== 1) return { error: "The existing Split Worker membership is invalid." };
|
|
55
|
+
if (!session.sessionOriginId || !roles.workers[0].sessionOriginId || !Number.isInteger(generationOf(roles.workers[0]))) {
|
|
56
|
+
return { error: "The exact Driver and Split Worker identities are not durable enough to add another Worker." };
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
kind: "add",
|
|
60
|
+
group: group,
|
|
61
|
+
groupId: group.id,
|
|
62
|
+
ownerId: session.ownerId || null,
|
|
63
|
+
driverId: session.localId,
|
|
64
|
+
driverOriginId: session.sessionOriginId || null,
|
|
65
|
+
workerIds: roles.role.workerIds.slice(),
|
|
66
|
+
workers: roles.workers.map(function (worker) {
|
|
67
|
+
return { id: worker.localId, originId: worker.sessionOriginId || null, generation: generationOf(worker) };
|
|
68
|
+
}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function validateSnapshot(session, snapshot, requireSameObject) {
|
|
73
|
+
if (!snapshot || snapshot.kind === "initial") {
|
|
74
|
+
return ctx.isEligible(session) ? { ok: true, group: null } : { ok: false };
|
|
75
|
+
}
|
|
76
|
+
if (!multiWorkerFeature.isEnabled(ctx.multiWorkerFeature) || !ctx.canOffer(session) ||
|
|
77
|
+
(session.ownerId || null) !== snapshot.ownerId || session.localId !== snapshot.driverId ||
|
|
78
|
+
(session.sessionOriginId || null) !== snapshot.driverOriginId) return { ok: false };
|
|
79
|
+
var group = store.groupForMember(session.localId);
|
|
80
|
+
if (!group || group.id !== snapshot.groupId || requireSameObject && group !== snapshot.group) return { ok: false };
|
|
81
|
+
var roles = splitRoles.resolveLiveRoles(sm, group);
|
|
82
|
+
if (!roles || roles.role.kind === "adhoc" || roles.driver !== session || roles.role.workerIds.length !== 1 ||
|
|
83
|
+
roles.role.workerIds.join(",") !== snapshot.workerIds.join(",")) return { ok: false };
|
|
84
|
+
for (var i = 0; i < snapshot.workers.length; i++) {
|
|
85
|
+
var expected = snapshot.workers[i];
|
|
86
|
+
var worker = sm.sessions.get(expected.id);
|
|
87
|
+
if (!worker || roles.workers.indexOf(worker) === -1 ||
|
|
88
|
+
(worker.ownerId || null) !== snapshot.ownerId ||
|
|
89
|
+
(worker.sessionOriginId || null) !== expected.originId || generationOf(worker) !== expected.generation) {
|
|
90
|
+
return { ok: false };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { ok: true, group: group, roles: roles };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function snapshotFromProposal(proposal) {
|
|
97
|
+
if (!proposal || proposal.action !== "add") return { kind: "initial" };
|
|
98
|
+
return {
|
|
99
|
+
kind: "add",
|
|
100
|
+
groupId: proposal.sourceGroupId,
|
|
101
|
+
ownerId: proposal.sourceOwnerId || null,
|
|
102
|
+
driverId: proposal.sourceDriverId,
|
|
103
|
+
driverOriginId: proposal.sourceDriverOriginId || null,
|
|
104
|
+
workerIds: Array.isArray(proposal.sourceWorkerIds) ? proposal.sourceWorkerIds.slice() : [],
|
|
105
|
+
workers: Array.isArray(proposal.sourceWorkers) ? proposal.sourceWorkers.slice() : [],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function propose(args, session) {
|
|
110
|
+
var snapshot = initialSnapshot(session);
|
|
111
|
+
if (snapshot.error) return ctx.toolResult({ error: snapshot.error });
|
|
112
|
+
var superseded = proposalControl.pendingProposal(session);
|
|
113
|
+
var supersedeId = typeof args.supersedeProposalId === "string" ? args.supersedeProposalId.trim() : "";
|
|
114
|
+
if (superseded && superseded.proposalId !== supersedeId) return ctx.toolResult({ error: "A Split Worker suggestion is already awaiting a decision. Inspect it or supply its exact id to supersede it." });
|
|
115
|
+
if (!superseded && supersedeId) return ctx.toolResult({ error: "The Split Worker suggestion to supersede is no longer pending." });
|
|
116
|
+
if (superseded && superseded.status !== "pending") return ctx.toolResult({ error: "The Split Worker suggestion is already starting and cannot be superseded." });
|
|
117
|
+
var summary = typeof args.summary === "string" ? args.summary.trim() : "";
|
|
118
|
+
var plan = typeof args.plan === "string" ? args.plan.trim() : "";
|
|
119
|
+
var task = typeof args.message === "string" ? args.message.trim() : "";
|
|
120
|
+
var rationale = typeof args.recommendationRationale === "string" ? args.recommendationRationale.trim() : "";
|
|
121
|
+
if (!summary || !plan || !task || !rationale) return ctx.toolResult({ error: "summary, plan, message, and recommendationRationale are required." });
|
|
122
|
+
if (task.length > ctx.maxTaskChars) return ctx.toolResult({ error: "The Split Worker task is too long." });
|
|
123
|
+
await ctx.ensureModelCatalogs();
|
|
124
|
+
if (!validateSnapshot(session, snapshot, true).ok) {
|
|
125
|
+
return ctx.toolResult({ error: "The Driver/Split Worker membership changed while runtimes were loading." });
|
|
126
|
+
}
|
|
127
|
+
if (proposalControl.pendingProposal(session) !== superseded) return ctx.toolResult({ error: "The pending Split Worker suggestion changed while runtimes were loading." });
|
|
128
|
+
var options = ctx.proposalOptions();
|
|
129
|
+
if (options.installedVendors.length === 0) return ctx.toolResult({ error: "No coding agent is installed for a Split Worker session." });
|
|
130
|
+
var recommendation = ctx.chooseRecommendation(args, session, options);
|
|
131
|
+
var proposal = {
|
|
132
|
+
type: "worker_proposal", proposalId: "worker_" + crypto.randomUUID(),
|
|
133
|
+
summary: summary.slice(0, ctx.maxSummaryChars), plan: plan.slice(0, ctx.maxPlanChars), message: task,
|
|
134
|
+
status: "pending", createdAt: Date.now(), recommendedVendor: recommendation.vendor,
|
|
135
|
+
recommendedModel: recommendation.model, recommendedEffort: recommendation.effort,
|
|
136
|
+
recommendationRationale: rationale.slice(0, ctx.maxRationaleChars), options: options,
|
|
137
|
+
};
|
|
138
|
+
if (snapshot.kind === "add") Object.assign(proposal, {
|
|
139
|
+
action: "add", sourceGroupId: snapshot.groupId, sourceOwnerId: snapshot.ownerId,
|
|
140
|
+
sourceDriverId: snapshot.driverId, sourceDriverOriginId: snapshot.driverOriginId,
|
|
141
|
+
sourceWorkerIds: snapshot.workerIds.slice(), sourceWorkers: snapshot.workers.slice(),
|
|
142
|
+
});
|
|
143
|
+
if (superseded) ctx.updateProposal(session, superseded, { status: "superseded", supersededBy: proposal.proposalId });
|
|
144
|
+
sm.sendAndRecord(session, proposal);
|
|
145
|
+
if (ctx.skipPermissionsEnabled(session) && ctx.recommendationCanAutoAccept(args, recommendation, options)) {
|
|
146
|
+
var accepted = await ctx.acceptProposal(session, proposal, {
|
|
147
|
+
vendor: recommendation.vendor, model: recommendation.model, effort: recommendation.effort,
|
|
148
|
+
}, ctx.autoAcceptanceWs(session), true);
|
|
149
|
+
return ctx.toolResult({ status: accepted.ok ? "auto_accepted" : "posted", proposalId: proposal.proposalId,
|
|
150
|
+
instruction: accepted.ok ? "The recorded Split Worker configuration was auto-accepted. End this turn while the exact Worker runs." :
|
|
151
|
+
"The automatic decision failed closed. The configuration card remains pending for the user." });
|
|
152
|
+
}
|
|
153
|
+
return ctx.toolResult({ status: "posted", proposalId: proposal.proposalId,
|
|
154
|
+
instruction: "The Split Worker suggestion is visible in the chat. End this turn now and wait for the user's decision." });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function canMountProposal(session) {
|
|
158
|
+
if (!ctx.canOffer(session)) return false;
|
|
159
|
+
var group = store.groupForMember(session.localId);
|
|
160
|
+
if (!group) return true;
|
|
161
|
+
if (!multiWorkerFeature.isEnabled(ctx.multiWorkerFeature)) return false;
|
|
162
|
+
var roles = splitRoles.resolveLiveRoles(sm, group);
|
|
163
|
+
return !!roles && roles.role.kind !== "adhoc" && roles.driver === session;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
propose: propose,
|
|
168
|
+
canMountProposal: canMountProposal,
|
|
169
|
+
validateAcceptance: function (session, proposal) {
|
|
170
|
+
return validateSnapshot(session, snapshotFromProposal(proposal), false);
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = { attachWorkerCreationProposal: attachWorkerCreationProposal,
|
|
176
|
+
captureMembership: captureMembership, matchesMembership: matchesMembership };
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
// same user. Anything else is refused and the request stays pending.
|
|
44
44
|
|
|
45
45
|
var crypto = require("crypto");
|
|
46
|
+
var splitRoles = require("./session-split-group-roles");
|
|
46
47
|
|
|
47
48
|
// Tools that collect input from the person rather than authorize an action.
|
|
48
49
|
// These are not permission approvals, so they are never routed to a Driver;
|
|
@@ -104,20 +105,18 @@ function attachWorkerPermission(ctx) {
|
|
|
104
105
|
if (sm.sessions.get(session.localId) !== session) return null;
|
|
105
106
|
var group = store.groupForMember(session.localId);
|
|
106
107
|
if (!group || !group.pair) return null;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if ((driver.ownerId || null) !== (session.ownerId || null)) return null;
|
|
111
|
-
if (driver.destroying) return null;
|
|
112
|
-
return { group: group, driver: driver };
|
|
108
|
+
var roles = splitRoles.resolveLiveRoles(sm, group);
|
|
109
|
+
if (!roles || roles.workers.indexOf(session) === -1) return null;
|
|
110
|
+
return { group: group, driver: roles.driver };
|
|
113
111
|
}
|
|
114
112
|
|
|
115
113
|
// Is the record's pair still exactly the pair it was created for?
|
|
116
114
|
function pairStillExact(record) {
|
|
117
115
|
var group = store ? store.groupForMember(record.workerId) : null;
|
|
118
116
|
if (!group || group.id !== record.groupId || !group.pair) return false;
|
|
119
|
-
|
|
120
|
-
if (
|
|
117
|
+
var roles = splitRoles.resolveLiveRoles(sm, group);
|
|
118
|
+
if (!roles || roles.driver.localId !== record.driverId) return false;
|
|
119
|
+
if (roles.workers.indexOf(record.workerRef) === -1 || record.workerRef.localId !== record.workerId) return false;
|
|
121
120
|
var driver = sm.sessions.get(record.driverId);
|
|
122
121
|
var worker = sm.sessions.get(record.workerId);
|
|
123
122
|
if (!driver || !worker || driver.destroying || worker.destroying) return false;
|