clay-server 4.3.0-beta.5 → 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-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 +41 -11
- package/lib/project-pair-result-delivery.js +4 -3
- package/lib/project-pair-result-outbox.js +73 -4
- package/lib/project-pair-result-recovery.js +6 -2
- package/lib/project-pair-structural-close.js +20 -0
- package/lib/project-pair-task-control.js +39 -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 +55 -78
- 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 +15 -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 +4 -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 +8 -3
- package/lib/session-pair-prompts.js +34 -24
- 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/ws-schema.js +2 -0
- package/package.json +1 -1
|
@@ -1,45 +1,38 @@
|
|
|
1
|
-
var pairMcp = require("./session-pair-mcp-server")
|
|
2
|
-
var driverEligibility = require("./session-driver-eligibility"),
|
|
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;
|
|
3
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;
|
|
4
6
|
var pairHistory = require("./session-pair-history"), responseText = pairHistory.responseText, errorSince = pairHistory.errorSince, recentTurns = pairHistory.recentTurns, operationFingerprint = require("./session-pair-operation").fingerprint;
|
|
5
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;
|
|
6
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 }); }
|
|
7
10
|
function attachSessionPair(ctx) {
|
|
8
|
-
var sm = ctx.sm, store = ctx.splitStore, workerProposal, workerPermission, lifecycle, taskControl;
|
|
11
|
+
var sm = ctx.sm, store = ctx.splitStore, workerProposal, workerPermission, lifecycle, taskControl, closeTransaction;
|
|
9
12
|
var turnControl = attachPairTurnControl({ sm: sm, splitStore: store }), resultCapture;
|
|
13
|
+
function targetOptions(options) { return Object.assign({}, options || {}, { multiWorkerFeature: ctx.multiWorkerFeature }); }
|
|
10
14
|
function groupAndPartner(caller, missingPairMessage) {
|
|
15
|
+
var args = arguments[2];
|
|
16
|
+
var options = arguments[3];
|
|
11
17
|
if (!caller) throw new Error("partner tools require a session-bound tool server");
|
|
12
18
|
if (sm.sessions.get(caller.localId) !== caller) {
|
|
13
19
|
throw new Error("this session is no longer live; the partner tools are bound to an exact session");
|
|
14
20
|
}
|
|
15
21
|
var group = store.groupForMember(caller.localId);
|
|
16
22
|
if (!group) throw new Error(missingPairMessage || "this session is not in a split group");
|
|
17
|
-
|
|
18
|
-
throw new Error("only the configured Driver can direct this pair");
|
|
19
|
-
}
|
|
20
|
-
var partnerId = group.members[0] === caller.localId ? group.members[1] : group.members[0];
|
|
21
|
-
var partner = sm.sessions.get(partnerId);
|
|
22
|
-
if (!partner) throw new Error("split partner session was not found");
|
|
23
|
-
if (caller.ownerId !== partner.ownerId) throw new Error("split partner access denied");
|
|
24
|
-
return { group: group, partner: partner };
|
|
23
|
+
return pairTarget.resolveGroupTarget(sm, group, caller, args, targetOptions(options));
|
|
25
24
|
}
|
|
26
25
|
function rolesFor(session) {
|
|
27
|
-
|
|
28
|
-
var group = store.groupForMember(session.localId);
|
|
29
|
-
if (!group || !group.pair) return null;
|
|
30
|
-
var driver = sm.sessions.get(group.pair.driverId);
|
|
31
|
-
var worker = sm.sessions.get(group.pair.workerId);
|
|
32
|
-
if (!driver || !worker || driver.ownerId !== worker.ownerId) return null;
|
|
33
|
-
return { group: group, driver: driver, worker: worker };
|
|
26
|
+
return pairTarget.rolesFor(sm, store, session);
|
|
34
27
|
}
|
|
35
28
|
var pairMessage = attachPairMessage({
|
|
36
29
|
sm: sm,
|
|
37
30
|
getSdk: ctx.getSdk,
|
|
38
|
-
resolvePair: groupAndPartner,
|
|
31
|
+
resolvePair: function (caller, args) { return groupAndPartner(caller, null, args, { mutation: true }); },
|
|
39
32
|
turnControl: turnControl,
|
|
40
33
|
});
|
|
41
34
|
taskControl = attachPairTaskControl({
|
|
42
|
-
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,
|
|
43
36
|
sendFollowup: function (args, caller) { return sendToPartner(args, caller); },
|
|
44
37
|
});
|
|
45
38
|
function broadcastDelegation(group, caller, partner, active) {
|
|
@@ -120,11 +113,13 @@ function attachSessionPair(ctx) {
|
|
|
120
113
|
var token = partner && partner._pairDelegation;
|
|
121
114
|
if (!token) return false;
|
|
122
115
|
if (token._capturePending || token._captureComplete) return false;
|
|
116
|
+
if (!taskControl.matchesGeneration(partner, token)) return false;
|
|
123
117
|
var group = store.groupForMember(partner.localId);
|
|
124
118
|
if (!group || group.id !== token.groupId || group.members.indexOf(token.from) === -1) return false;
|
|
125
119
|
var caller = sm.sessions.get(token.from);
|
|
126
120
|
if (!caller || caller.ownerId !== partner.ownerId) return false;
|
|
127
|
-
|
|
121
|
+
var liveRoles = splitRoles.resolveLiveRoles(sm, group);
|
|
122
|
+
if (group.pair && (!liveRoles || liveRoles.driver !== caller || liveRoles.workers.indexOf(partner) === -1)) {
|
|
128
123
|
finishDelegation(group, caller, partner, token);
|
|
129
124
|
return false;
|
|
130
125
|
}
|
|
@@ -140,7 +135,8 @@ function attachSessionPair(ctx) {
|
|
|
140
135
|
var deadline = Date.now() + timeoutSeconds * 1000;
|
|
141
136
|
var timer = setInterval(function () {
|
|
142
137
|
var currentGroup = store.groupForMember(caller.localId);
|
|
143
|
-
|
|
138
|
+
var exactTarget = splitRoles.matchesLiveTarget(sm, currentGroup, caller, partner);
|
|
139
|
+
if (!currentGroup || currentGroup.id !== group.id || !exactTarget) {
|
|
144
140
|
clearInterval(timer);
|
|
145
141
|
finishDelegation(group, caller, partner, token);
|
|
146
142
|
reject(new Error("the split group was dissolved while waiting for the partner"));
|
|
@@ -148,6 +144,7 @@ function attachSessionPair(ctx) {
|
|
|
148
144
|
}
|
|
149
145
|
if (!partner.isProcessing && !partner._queryStarting) {
|
|
150
146
|
clearInterval(timer);
|
|
147
|
+
if (!taskControl.matchesGeneration(partner, token)) return reject(new Error("the delegated task no longer matches this Worker generation"));
|
|
151
148
|
var failure = errorSince(partner.history || [], token.startIndex);
|
|
152
149
|
var interrupted = !failure && !!partner._lastTurnInterrupted;
|
|
153
150
|
var response = responseText(partner.history || [], token.startIndex);
|
|
@@ -182,18 +179,20 @@ function attachSessionPair(ctx) {
|
|
|
182
179
|
function monitorPartner(group, caller, partner, token) {
|
|
183
180
|
var timer = setInterval(function () {
|
|
184
181
|
var currentGroup = store.groupForMember(caller.localId);
|
|
185
|
-
|
|
182
|
+
var exactTarget = splitRoles.matchesLiveTarget(sm, currentGroup, caller, partner);
|
|
183
|
+
if (!currentGroup || currentGroup.id !== group.id || !exactTarget || (!partner.isProcessing && !partner._queryStarting)) {
|
|
186
184
|
clearInterval(timer);
|
|
187
|
-
if (currentGroup && currentGroup.id === group.id) handleTurnDone(partner);
|
|
185
|
+
if (currentGroup && currentGroup.id === group.id && exactTarget) handleTurnDone(partner);
|
|
188
186
|
else finishDelegation(group, caller, partner, token);
|
|
189
187
|
}
|
|
190
188
|
}, 500);
|
|
189
|
+
if (timer && typeof timer.unref === "function") timer.unref();
|
|
191
190
|
}
|
|
192
191
|
async function sendToPartner(args, caller) {
|
|
193
192
|
try {
|
|
194
193
|
if (caller && caller._delegatedBy) throw new Error("delegated turns cannot delegate to another session");
|
|
195
194
|
var resolved = groupAndPartner(caller,
|
|
196
|
-
"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 });
|
|
197
196
|
var message = typeof args.message === "string" ? args.message.trim() : "";
|
|
198
197
|
if (!message) throw new Error("message is required");
|
|
199
198
|
turnControl.assertWorkerAction(caller);
|
|
@@ -248,6 +247,7 @@ function attachSessionPair(ctx) {
|
|
|
248
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." });
|
|
249
248
|
}
|
|
250
249
|
if (!partner.isProcessing && !partner._queryStarting) {
|
|
250
|
+
if (!taskControl.matchesGeneration(partner, token)) return toolError(new Error("the delegated task no longer matches this Worker generation"));
|
|
251
251
|
var immediateFailure = errorSince(partner.history || [], token.startIndex);
|
|
252
252
|
var immediateResponse = responseText(partner.history || [], token.startIndex);
|
|
253
253
|
var immediateOutcome = taskControl.complete(partner, caller, token, immediateFailure ? "failed" : "completed", immediateResponse, immediateFailure);
|
|
@@ -268,8 +268,9 @@ function attachSessionPair(ctx) {
|
|
|
268
268
|
if (caller) {
|
|
269
269
|
var group = store.groupForMember(caller.localId);
|
|
270
270
|
if (group) {
|
|
271
|
-
var
|
|
272
|
-
var
|
|
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;
|
|
273
274
|
if (partner && partner._pairDelegation && partner._pairDelegation.from === caller.localId && !partner.isProcessing && !partner._pairDelegation._retainOnError) {
|
|
274
275
|
finishDelegation(group, caller, partner, partner._pairDelegation);
|
|
275
276
|
}
|
|
@@ -280,7 +281,7 @@ function attachSessionPair(ctx) {
|
|
|
280
281
|
}
|
|
281
282
|
function readPartner(args, caller) {
|
|
282
283
|
try {
|
|
283
|
-
var resolved = groupAndPartner(caller);
|
|
284
|
+
var resolved = groupAndPartner(caller, null, args, { readOnly: true });
|
|
284
285
|
var count = Number.isFinite(args.lastTurns) ? Math.floor(args.lastTurns) : 1;
|
|
285
286
|
count = Math.max(0, Math.min(5, count));
|
|
286
287
|
var payload = {
|
|
@@ -289,7 +290,7 @@ function attachSessionPair(ctx) {
|
|
|
289
290
|
title: resolved.partner.title || "New Session",
|
|
290
291
|
turns: count > 0 ? recentTurns(resolved.partner, count) : [],
|
|
291
292
|
};
|
|
292
|
-
payload.capacity = lifecycle.optionalStatus(caller);
|
|
293
|
+
payload.capacity = lifecycle.optionalStatus(caller, args);
|
|
293
294
|
return toolResult(payload);
|
|
294
295
|
} catch (e) {
|
|
295
296
|
return toolError(e);
|
|
@@ -297,7 +298,7 @@ function attachSessionPair(ctx) {
|
|
|
297
298
|
}
|
|
298
299
|
function interruptPartner(args, caller) {
|
|
299
300
|
try {
|
|
300
|
-
var resolved = groupAndPartner(caller);
|
|
301
|
+
var resolved = groupAndPartner(caller, null, args, { mutation: true });
|
|
301
302
|
var partner = resolved.partner;
|
|
302
303
|
if (!partner.isProcessing && !partner._queryStarting) {
|
|
303
304
|
return toolResult({ status: "idle", partnerId: partner.localId, title: partner.title || "New Session" });
|
|
@@ -310,43 +311,26 @@ function attachSessionPair(ctx) {
|
|
|
310
311
|
return toolError(e);
|
|
311
312
|
}
|
|
312
313
|
}
|
|
314
|
+
function closeWorker(args, caller) {
|
|
315
|
+
return closeTransaction.close(args, caller);
|
|
316
|
+
}
|
|
313
317
|
function closePartner(args, caller) {
|
|
314
318
|
try {
|
|
315
|
-
|
|
316
|
-
var partner = resolved.partner;
|
|
317
|
-
var interrupted = !!(partner.isProcessing || partner._queryStarting);
|
|
318
|
-
if (interrupted) {
|
|
319
|
-
taskControl.markInterruption(partner, "driver", "The Driver closed the Split Worker pair.");
|
|
320
|
-
partner.taskStopRequested = true;
|
|
321
|
-
if (partner.abortController) partner.abortController.abort();
|
|
322
|
-
}
|
|
323
|
-
if (partner._pairDelegation) {
|
|
324
|
-
completeInterruptedTask(partner, caller, partner._pairDelegation);
|
|
325
|
-
finishDelegation(resolved.group, caller, partner, partner._pairDelegation);
|
|
326
|
-
}
|
|
327
|
-
workerPermission.cancelForSession(partner, "The Driver closed the Split Worker pair.");
|
|
328
|
-
var ws = { _clayUser: caller.ownerId ? { id: caller.ownerId } : null };
|
|
329
|
-
var result = store.dissolve(ws, { id: resolved.group.id });
|
|
330
|
-
if (!result.ok) throw new Error(result.error || "could not close the Worker pair");
|
|
331
|
-
return toolResult({ status: "closed", partnerId: partner.localId, interrupted: interrupted, historyPreserved: true });
|
|
319
|
+
return toolResult(closeWorker(args, caller));
|
|
332
320
|
} catch (e) {
|
|
333
321
|
return toolError(e);
|
|
334
322
|
}
|
|
335
323
|
}
|
|
336
324
|
function handleHumanStop(session) {
|
|
337
325
|
var roles = turnControl.markHumanStop(session);
|
|
338
|
-
|
|
339
|
-
if (roles.worker._pairDelegation && roles.worker._pairDelegation.outboxKey && ctx.resultOutbox) ctx.resultOutbox.markBlocked(roles.worker._pairDelegation.outboxKey, "The human stopped this Split Worker turn.", "human_stop");
|
|
340
|
-
taskControl.markInterruption(roles.worker, "user", "The human stopped this Split Worker turn.");
|
|
341
|
-
workerPermission.cancelForSession(roles.worker, "The human stopped this Split Worker turn.");
|
|
342
|
-
return true;
|
|
326
|
+
return stopAllWorkers(session, roles, { resultOutbox: ctx.resultOutbox, taskControl: taskControl, workerPermission: workerPermission });
|
|
343
327
|
}
|
|
344
328
|
function stopOwnedPair(session, reason) { return stopOwned(session, rolesFor(session), turnControl, taskControl, workerPermission, reason); }
|
|
345
329
|
function stopAutonomousWork(session) { return stopAutonomous(session, rolesFor(session), turnControl, taskControl, workerPermission); }
|
|
346
330
|
function beginHumanTurn(session) {
|
|
347
331
|
var resumed = turnControl.beginHumanTurn(session);
|
|
348
332
|
var roles = resumed && rolesFor(session);
|
|
349
|
-
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]);
|
|
350
334
|
return resumed;
|
|
351
335
|
}
|
|
352
336
|
function stopBarrier(session) { return turnControl.blockedReason(session); }
|
|
@@ -362,7 +346,8 @@ function attachSessionPair(ctx) {
|
|
|
362
346
|
evaluate: function () { return toolError(new Error("record_partner_evaluation requires a session-bound tool server")); },
|
|
363
347
|
});
|
|
364
348
|
var group = store.groupForMember(boundSession.localId);
|
|
365
|
-
|
|
349
|
+
var boundRoles = group && group.pair ? splitRoles.resolveLiveRoles(sm, group) : null;
|
|
350
|
+
if (boundRoles && boundRoles.workers.indexOf(boundSession) !== -1) return taskControl.workerToolDefs(boundSession);
|
|
366
351
|
var adHocSplit = !!(group && !group.pair);
|
|
367
352
|
if (!driverEligibility.isEligibleDriverSession(boundSession, sm)) return [];
|
|
368
353
|
var persistentCatalog = boundSession.vendor === "codex" && (!group || !!group.pair);
|
|
@@ -375,7 +360,7 @@ function attachSessionPair(ctx) {
|
|
|
375
360
|
send: function (args) {
|
|
376
361
|
return turnControl.runOperation(boundSession, "send", args && args.operationId, function () {
|
|
377
362
|
return sendToPartner(args || {}, boundSession);
|
|
378
|
-
}, operationFingerprint(args, ["message", "wait", "timeoutSeconds", "taskId"])).catch(toolError);
|
|
363
|
+
}, operationFingerprint(args, ["message", "wait", "timeoutSeconds", "taskId", "workerId"])).catch(toolError);
|
|
379
364
|
},
|
|
380
365
|
read: function (args) { return readPartner(args, boundSession); },
|
|
381
366
|
message: function (args) {
|
|
@@ -391,7 +376,7 @@ function attachSessionPair(ctx) {
|
|
|
391
376
|
replace: function (args) {
|
|
392
377
|
return turnControl.runOperation(boundSession, "replace", args && args.operationId, function () {
|
|
393
378
|
return workerProposal.proposeReplacement(args || {}, boundSession);
|
|
394
|
-
}, 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);
|
|
395
380
|
},
|
|
396
381
|
evaluate: lifecycleHandlers.evaluate,
|
|
397
382
|
}, { lifecycle: persistentCatalog || !adHocSplit });
|
|
@@ -412,12 +397,14 @@ function attachSessionPair(ctx) {
|
|
|
412
397
|
}
|
|
413
398
|
var factory = attachPairFactory(ctx), createPairRecord = factory.createPairRecord,
|
|
414
399
|
createWorkerForDriver = factory.createWorkerForDriver, createPair = factory.createPair,
|
|
400
|
+
addWorkerForDriver = factory.addWorkerForDriver, replaceWorkerForDriver = factory.replaceWorkerForDriver,
|
|
415
401
|
preflightWorkerForDriver = factory.preflightWorkerForDriver, createOwnedPair = factory.createOwnedPair, removeOwnedPair = factory.removeOwnedPair,
|
|
416
402
|
preflightRuntime = factory.preflightRuntime;
|
|
417
403
|
lifecycle = attachPairLifecycle({
|
|
418
404
|
sm: sm,
|
|
419
405
|
splitStore: store,
|
|
420
406
|
createWorkerForDriver: function (driver, args) { return createWorkerForDriver(driver, args); },
|
|
407
|
+
replaceWorkerForDriver: function (driver, worker, args) { return replaceWorkerForDriver(driver, worker, args); },
|
|
421
408
|
preflightWorkerForDriver: function (driver, args) { return preflightWorkerForDriver(driver, args); },
|
|
422
409
|
sendToPartner: function (args, caller) { return sendToPartner(args, caller); },
|
|
423
410
|
finishDelegation: finishDelegation,
|
|
@@ -427,6 +414,7 @@ function attachSessionPair(ctx) {
|
|
|
427
414
|
taskStatus: function (worker) { return taskControl.status(worker); },
|
|
428
415
|
proposalStatus: function (driver) { return workerProposal ? workerProposal.proposalStatus(driver) : null; },
|
|
429
416
|
turnControl: turnControl,
|
|
417
|
+
multiWorkerFeature: ctx.multiWorkerFeature,
|
|
430
418
|
});
|
|
431
419
|
workerPermission = attachWorkerPermission({
|
|
432
420
|
sm: sm,
|
|
@@ -450,7 +438,9 @@ function attachSessionPair(ctx) {
|
|
|
450
438
|
recordGenerationStart: function (driver, worker) { return lifecycle.recordGenerationStart(driver, worker); },
|
|
451
439
|
sendToPartner: sendToPartner,
|
|
452
440
|
replacePartner: function (args, session) { return lifecycle.replacePartner(args, session); },
|
|
441
|
+
addWorkerForDriver: function (driver, args) { return addWorkerForDriver(driver, args); },
|
|
453
442
|
preflightWorkerForDriver: preflightWorkerForDriver,
|
|
443
|
+
multiWorkerFeature: ctx.multiWorkerFeature,
|
|
454
444
|
});
|
|
455
445
|
function handleMessage(ws, msg) {
|
|
456
446
|
if (workerProposal.handleMessage(ws, msg)) return true;
|
|
@@ -471,28 +461,15 @@ function attachSessionPair(ctx) {
|
|
|
471
461
|
}
|
|
472
462
|
return false;
|
|
473
463
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (roles && roles.worker === session) {
|
|
477
|
-
var taskId = session._pairDelegation && session._pairDelegation.taskId || "unknown";
|
|
478
|
-
return pairPrompts.worker(taskId);
|
|
479
|
-
}
|
|
480
|
-
var group = store.groupForMember(session.localId);
|
|
481
|
-
var pairPrompt = "";
|
|
482
|
-
if (group && group.pair && group.pair.driverId === session.localId) {
|
|
483
|
-
pairPrompt = pairPrompts.DRIVER_CORE;
|
|
484
|
-
if (driverOrchestration.isHighTierDriverSession(session, sm)) {
|
|
485
|
-
pairPrompt += " " + pairPrompts.DRIVER_DELEGATION;
|
|
486
|
-
}
|
|
487
|
-
} else if (!group && !ctx.isMate && driverEligibility.isEligibleDriverSession(session, sm) &&
|
|
488
|
-
driverOrchestration.isHighTierDriverSession(session, sm)) {
|
|
489
|
-
pairPrompt = pairPrompts.UNPAIRED + " " + workerProposal.getSystemPrompt(session);
|
|
490
|
-
}
|
|
491
|
-
return pairPrompt;
|
|
492
|
-
}
|
|
464
|
+
var pairPrompt = attachSessionPairPrompt({ sm: sm, store: store, isMate: ctx.isMate, rolesFor: rolesFor,
|
|
465
|
+
workerProposal: function () { return workerProposal; } });
|
|
493
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); } });
|
|
494
467
|
resultCapture = resultPipeline.capture; var resultDelivery = resultPipeline.delivery;
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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 }; } };
|
|
498
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;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
function attachProposalRunner(ctx) {
|
|
2
|
+
function resumeDriver(session, text) {
|
|
3
|
+
var sdk = ctx.getSdk();
|
|
4
|
+
if (!sdk) return Promise.reject(new Error("SDK bridge is not ready"));
|
|
5
|
+
ctx.sm.sendAndRecord(session, { type: "user_message", text: text, _internal: true });
|
|
6
|
+
session.sentToolResults = {};
|
|
7
|
+
if (!session.isProcessing) {
|
|
8
|
+
session.isProcessing = true;
|
|
9
|
+
ctx.onProcessingChanged();
|
|
10
|
+
ctx.sm.sendToSession(session, { type: "status", status: "processing" });
|
|
11
|
+
}
|
|
12
|
+
if (sdk.pushMessage(session, text)) return Promise.resolve();
|
|
13
|
+
return Promise.resolve(sdk.startQuery(session, text, undefined, ctx.getLinuxUserForSession(session)));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseResult(result) {
|
|
17
|
+
if (!result || result.isError || !result.content || !result.content[0]) {
|
|
18
|
+
return { status: "error", error: "Split Worker execution failed." };
|
|
19
|
+
}
|
|
20
|
+
try { return JSON.parse(result.content[0].text); }
|
|
21
|
+
catch (error) { return { status: "error", error: result.content[0].text || "Split Worker execution failed." }; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function runWorker(session, proposal) {
|
|
25
|
+
var result = parseResult(await ctx.sendToPartner({ message: proposal.message, wait: true,
|
|
26
|
+
timeoutSeconds: 900, workerId: proposal.workerId }, session));
|
|
27
|
+
ctx.updateProposal(session, proposal, { status: result.status === "complete" ? "completed" : result.status || "error",
|
|
28
|
+
resultPreview: result.response ? result.response.slice(0, 1200) : "", error: result.error || null });
|
|
29
|
+
var followup;
|
|
30
|
+
if (result.status === "complete") {
|
|
31
|
+
followup = "[Split Worker execution completed]\nReview and verify the Split Worker's result. The Split Worker session remains available: if the implementation needs corrections or additional edits, send a follow-up with send_to_partner instead of taking over the Split Worker-owned files yourself. If that Split Worker is no longer available, keep the work in the visible Split Worker flow and use send_to_partner again after the stale pair is removed; never substitute a background Sub-agent.\n\n" + (result.response || "The Split Worker completed without a text summary.");
|
|
32
|
+
} else if (result.status === "interrupted") {
|
|
33
|
+
followup = "[Split Worker execution interrupted]\nThe Split Worker stopped mid-turn. Its work is PARTIAL and unverified — do not treat it as finished. Check partner_status before deciding next steps; this result alone does not identify who interrupted it. If the human stopped it, do not retry until a new human Driver message. Otherwise continue within the user's authorized task.";
|
|
34
|
+
} else if (result.status === "running") {
|
|
35
|
+
followup = "[Split Worker execution is still running]\nUse read_partner to inspect progress before completing the task.";
|
|
36
|
+
} else {
|
|
37
|
+
followup = "[Split Worker execution failed]\nThe existing Split Worker may still be reusable. Inspect partner_status and send a narrower follow-up with send_to_partner when the Worker and pair still exist; do not replace it merely because its task or transport failed. Retry replacement only when the replacement transaction reports that no new Worker was created and the failure is explicitly safe to retry.\n\n" + (result.error || result.response || "Unknown Split Worker error.");
|
|
38
|
+
}
|
|
39
|
+
await resumeDriver(session, followup);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { resumeDriver: resumeDriver, runWorker: runWorker };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { attachProposalRunner: attachProposalRunner };
|