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.
Files changed (48) hide show
  1. package/lib/driver-continuation-pair.js +5 -1
  2. package/lib/multi-worker-feature.js +11 -0
  3. package/lib/project-pair-autonomous-stop.js +14 -8
  4. package/lib/project-pair-close-transaction.js +59 -0
  5. package/lib/project-pair-global-stop.js +18 -0
  6. package/lib/project-pair-lifecycle-status.js +34 -0
  7. package/lib/project-pair-lifecycle.js +58 -76
  8. package/lib/project-pair-message.js +1 -1
  9. package/lib/project-pair-owned-stop.js +7 -4
  10. package/lib/project-pair-result-capture.js +41 -11
  11. package/lib/project-pair-result-delivery.js +4 -3
  12. package/lib/project-pair-result-outbox.js +73 -4
  13. package/lib/project-pair-result-recovery.js +6 -2
  14. package/lib/project-pair-structural-close.js +20 -0
  15. package/lib/project-pair-task-control.js +39 -18
  16. package/lib/project-pair-worker-close.js +68 -0
  17. package/lib/project-session-pair-prompt.js +26 -0
  18. package/lib/project-session-pair.js +55 -78
  19. package/lib/project-worker-creation-proposal.js +176 -0
  20. package/lib/project-worker-permission.js +7 -8
  21. package/lib/project-worker-proposal-runner.js +45 -0
  22. package/lib/project-worker-proposal.js +79 -115
  23. package/lib/project.js +15 -0
  24. package/lib/public/css/pane.css +48 -2
  25. package/lib/public/modules/app-connection.js +49 -76
  26. package/lib/public/modules/app-messages.js +4 -0
  27. package/lib/public/modules/project-activation.js +10 -7
  28. package/lib/public/modules/sidebar-sessions.js +31 -11
  29. package/lib/public/modules/split-group-helpers.js +74 -2
  30. package/lib/public/modules/split-pair-ui.js +28 -6
  31. package/lib/public/modules/split-pane-reconciler.js +28 -0
  32. package/lib/public/modules/split-pane-renderer.js +107 -0
  33. package/lib/public/modules/split-view.js +58 -188
  34. package/lib/public/modules/websocket-watchdog.js +126 -0
  35. package/lib/public/modules/worker-pane-lock.js +13 -3
  36. package/lib/server.js +1 -0
  37. package/lib/session-pair-factory.js +66 -0
  38. package/lib/session-pair-mcp-server.js +8 -3
  39. package/lib/session-pair-prompts.js +34 -24
  40. package/lib/session-pair-target.js +42 -0
  41. package/lib/session-pair-turn-control.js +11 -6
  42. package/lib/session-split-group-anchors.js +89 -7
  43. package/lib/session-split-group-persistence.js +136 -0
  44. package/lib/session-split-group-roles.js +164 -0
  45. package/lib/session-split-group-v2-store.js +146 -0
  46. package/lib/session-split-groups.js +109 -99
  47. package/lib/ws-schema.js +2 -0
  48. package/package.json +1 -1
@@ -1,45 +1,38 @@
1
- var pairMcp = require("./session-pair-mcp-server"), pairPrompts = require("./session-pair-prompts"); var { attachPairFactory } = require("./session-pair-factory"), { attachPairLifecycle } = require("./project-pair-lifecycle"), { attachPairTurnControl } = require("./session-pair-turn-control");
2
- var driverEligibility = require("./session-driver-eligibility"), driverOrchestration = require("./session-driver-orchestration"), { attachWorkerPermission } = require("./project-worker-permission"), { attachWorkerProposal } = require("./project-worker-proposal");
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
- if (group.pair && group.pair.driverId !== caller.localId) {
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
- if (!session || sm.sessions.get(session.localId) !== session) return null;
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
- if (group.pair && (group.pair.driverId !== caller.localId || group.pair.workerId !== partner.localId)) {
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
- if (!currentGroup || currentGroup.id !== group.id || currentGroup.members.indexOf(partner.localId) === -1) {
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
- if (!currentGroup || currentGroup.id !== group.id || (!partner.isProcessing && !partner._queryStarting)) {
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 partnerId = group.members[0] === caller.localId ? group.members[1] : group.members[0];
272
- var partner = sm.sessions.get(partnerId);
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
- var resolved = groupAndPartner(caller);
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
- if (!roles) return false;
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.worker);
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
- if (group && group.pair && group.pair.driverId !== boundSession.localId) return taskControl.workerToolDefs(boundSession);
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
- function getSystemPrompt(session) {
475
- var roles = rolesFor(session);
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
- return { beginHumanTurn: beginHumanTurn, getToolDefs: getToolDefs, handleHumanStop: handleHumanStop, stopOwnedPair: stopOwnedPair, stopAutonomousWork: stopAutonomousWork, stopBarrier: stopBarrier, handleMessage: handleMessage, handleTurnDone: handleTurnDone, getSystemPrompt: getSystemPrompt,
496
- respondToWorkerProposal: workerProposal.respondToProposal, workerPermission: workerPermission, createOwnedPair: createOwnedPair, removeOwnedPair: removeOwnedPair, preflightRuntime: preflightRuntime,
497
- sendToPartner: sendToPartner, 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 }; } };
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
- if (group.pair.workerId !== session.localId) return null;
108
- var driver = sm.sessions.get(group.pair.driverId);
109
- if (!driver) return null;
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
- if (group.pair.driverId !== record.driverId) return false;
120
- if (group.pair.workerId !== record.workerId) return false;
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 };