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.
Files changed (54) hide show
  1. package/lib/driver-continuation-pair.js +5 -1
  2. package/lib/multi-worker-feature.js +11 -0
  3. package/lib/project-connection.js +2 -0
  4. package/lib/project-pair-autonomous-stop.js +14 -8
  5. package/lib/project-pair-close-transaction.js +59 -0
  6. package/lib/project-pair-global-stop.js +18 -0
  7. package/lib/project-pair-lifecycle-status.js +34 -0
  8. package/lib/project-pair-lifecycle.js +58 -76
  9. package/lib/project-pair-message.js +1 -1
  10. package/lib/project-pair-owned-stop.js +7 -4
  11. package/lib/project-pair-result-capture.js +116 -0
  12. package/lib/project-pair-result-delivery.js +140 -0
  13. package/lib/project-pair-result-outbox.js +400 -0
  14. package/lib/project-pair-result-pipeline.js +16 -0
  15. package/lib/project-pair-result-recovery.js +229 -0
  16. package/lib/project-pair-structural-close.js +20 -0
  17. package/lib/project-pair-task-control.js +45 -18
  18. package/lib/project-pair-worker-close.js +68 -0
  19. package/lib/project-session-pair-prompt.js +26 -0
  20. package/lib/project-session-pair.js +87 -106
  21. package/lib/project-worker-creation-proposal.js +176 -0
  22. package/lib/project-worker-permission.js +7 -8
  23. package/lib/project-worker-proposal-runner.js +45 -0
  24. package/lib/project-worker-proposal.js +79 -115
  25. package/lib/project.js +38 -1
  26. package/lib/public/app.js +1 -0
  27. package/lib/public/css/messages.css +12 -0
  28. package/lib/public/css/pane.css +48 -2
  29. package/lib/public/modules/app-connection.js +49 -76
  30. package/lib/public/modules/app-messages.js +11 -0
  31. package/lib/public/modules/pair-result-status.js +87 -0
  32. package/lib/public/modules/project-activation.js +10 -7
  33. package/lib/public/modules/sidebar-sessions.js +31 -11
  34. package/lib/public/modules/split-group-helpers.js +74 -2
  35. package/lib/public/modules/split-pair-ui.js +28 -6
  36. package/lib/public/modules/split-pane-reconciler.js +28 -0
  37. package/lib/public/modules/split-pane-renderer.js +107 -0
  38. package/lib/public/modules/split-view.js +58 -188
  39. package/lib/public/modules/websocket-watchdog.js +126 -0
  40. package/lib/public/modules/worker-pane-lock.js +13 -3
  41. package/lib/server.js +1 -0
  42. package/lib/session-pair-factory.js +66 -0
  43. package/lib/session-pair-mcp-server.js +10 -5
  44. package/lib/session-pair-prompts.js +41 -25
  45. package/lib/session-pair-target.js +42 -0
  46. package/lib/session-pair-turn-control.js +11 -6
  47. package/lib/session-split-group-anchors.js +89 -7
  48. package/lib/session-split-group-persistence.js +136 -0
  49. package/lib/session-split-group-roles.js +164 -0
  50. package/lib/session-split-group-v2-store.js +146 -0
  51. package/lib/session-split-groups.js +109 -99
  52. package/lib/sessions.js +33 -2
  53. package/lib/ws-schema.js +2 -0
  54. package/package.json +1 -1
@@ -1,52 +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"),
2
- { attachPairTurnControl } = require("./session-pair-turn-control");
3
- 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;
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
- function toolResult(value) { return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] }); }
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 store = ctx.splitStore;
12
- var workerProposal;
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
- if (group.pair && group.pair.driverId !== caller.localId) {
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
- if (!session || sm.sessions.get(session.localId) !== session) return null;
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.detachRequested = true;
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
- 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)) {
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
- finishDelegation(group, caller, partner, token);
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
- 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) {
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
- finishDelegation(group, caller, partner, token);
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.detached = true;
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 an interim status check." });
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
- 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)) {
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 !== false;
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 an interim status check." });
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
- finishDelegation(resolved.group, caller, partner, token);
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
- taskControl.drain(caller, partner);
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 partnerId = group.members[0] === caller.localId ? group.members[1] : group.members[0];
270
- var partner = sm.sessions.get(partnerId);
271
- if (partner && partner._pairDelegation && partner._pairDelegation.from === caller.localId && !partner.isProcessing) {
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
- var resolved = groupAndPartner(caller);
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
- if (!roles) return false;
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.worker);
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
- 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);
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
- function getSystemPrompt(session) {
472
- var roles = rolesFor(session);
473
- if (roles && roles.worker === session) {
474
- var taskId = session._pairDelegation && session._pairDelegation.taskId || "unknown";
475
- return pairPrompts.worker(taskId);
476
- }
477
- var group = store.groupForMember(session.localId);
478
- var pairPrompt = "";
479
- if (group && group.pair && group.pair.driverId === session.localId) {
480
- pairPrompt = pairPrompts.DRIVER_CORE;
481
- if (driverOrchestration.isHighTierDriverSession(session, sm)) {
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
- 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;