clay-server 4.2.0 → 4.3.0-beta.10

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 (84) hide show
  1. package/lib/driver-continuation-access.js +54 -0
  2. package/lib/driver-continuation-lease.js +52 -0
  3. package/lib/driver-continuation-pair-transfer.js +86 -0
  4. package/lib/driver-continuation-pair.js +86 -0
  5. package/lib/driver-continuation-record.js +40 -0
  6. package/lib/driver-continuation-startup.js +68 -0
  7. package/lib/driver-continuation-transaction.js +61 -0
  8. package/lib/driver-continuation-trigger.js +110 -0
  9. package/lib/multi-worker-feature.js +11 -0
  10. package/lib/project-connection.js +3 -1
  11. package/lib/project-driver-continuation.js +499 -0
  12. package/lib/project-message-delivery.js +20 -0
  13. package/lib/project-pair-autonomous-stop.js +14 -8
  14. package/lib/project-pair-close-transaction.js +59 -0
  15. package/lib/project-pair-global-stop.js +18 -0
  16. package/lib/project-pair-lifecycle-status.js +34 -0
  17. package/lib/project-pair-lifecycle.js +117 -135
  18. package/lib/project-pair-message.js +1 -1
  19. package/lib/project-pair-owned-stop.js +7 -4
  20. package/lib/project-pair-replacement-state.js +5 -1
  21. package/lib/project-pair-result-capture.js +116 -0
  22. package/lib/project-pair-result-delivery.js +140 -0
  23. package/lib/project-pair-result-outbox.js +400 -0
  24. package/lib/project-pair-result-pipeline.js +16 -0
  25. package/lib/project-pair-result-recovery.js +229 -0
  26. package/lib/project-pair-structural-close.js +20 -0
  27. package/lib/project-pair-task-control.js +45 -18
  28. package/lib/project-pair-worker-close.js +68 -0
  29. package/lib/project-session-handoff.js +48 -55
  30. package/lib/project-session-pair-prompt.js +26 -0
  31. package/lib/project-session-pair.js +87 -106
  32. package/lib/project-sessions.js +4 -0
  33. package/lib/project-user-message.js +21 -0
  34. package/lib/project-worker-creation-proposal.js +176 -0
  35. package/lib/project-worker-permission.js +7 -8
  36. package/lib/project-worker-proposal-runner.js +45 -0
  37. package/lib/project-worker-proposal.js +79 -115
  38. package/lib/project.js +87 -3
  39. package/lib/public/app.js +1 -0
  40. package/lib/public/css/driver-continuation.css +56 -0
  41. package/lib/public/css/messages.css +12 -0
  42. package/lib/public/css/mobile-nav.css +29 -14
  43. package/lib/public/css/overlays.css +54 -25
  44. package/lib/public/css/pane.css +66 -12
  45. package/lib/public/css/sidebar.css +62 -7
  46. package/lib/public/index.html +3 -3
  47. package/lib/public/modules/app-connection.js +165 -74
  48. package/lib/public/modules/app-messages.js +31 -3
  49. package/lib/public/modules/driver-continuation-state.js +76 -0
  50. package/lib/public/modules/driver-continuation.js +260 -0
  51. package/lib/public/modules/message-delivery-ui.js +63 -0
  52. package/lib/public/modules/message-delivery.js +163 -6
  53. package/lib/public/modules/pair-result-status.js +87 -0
  54. package/lib/public/modules/permission-control.js +10 -5
  55. package/lib/public/modules/project-activation.js +10 -7
  56. package/lib/public/modules/session-creation-cta.js +46 -0
  57. package/lib/public/modules/sidebar-mobile.js +42 -30
  58. package/lib/public/modules/sidebar-sessions.js +77 -52
  59. package/lib/public/modules/split-group-helpers.js +74 -2
  60. package/lib/public/modules/split-pair-ui.js +28 -6
  61. package/lib/public/modules/split-pane-reconciler.js +28 -0
  62. package/lib/public/modules/split-pane-renderer.js +107 -0
  63. package/lib/public/modules/split-view.js +58 -188
  64. package/lib/public/modules/websocket-lifecycle.js +178 -0
  65. package/lib/public/modules/websocket-watchdog.js +126 -0
  66. package/lib/public/modules/worker-pane-lock.js +13 -3
  67. package/lib/public/style.css +1 -0
  68. package/lib/sdk-bridge.js +77 -0
  69. package/lib/server.js +1 -0
  70. package/lib/session-handoff-discovery.js +443 -0
  71. package/lib/session-handoff-mcp-server.js +32 -4
  72. package/lib/session-pair-factory.js +66 -0
  73. package/lib/session-pair-mcp-server.js +10 -5
  74. package/lib/session-pair-prompts.js +46 -25
  75. package/lib/session-pair-target.js +42 -0
  76. package/lib/session-pair-turn-control.js +30 -9
  77. package/lib/session-split-group-anchors.js +126 -0
  78. package/lib/session-split-group-persistence.js +136 -0
  79. package/lib/session-split-group-roles.js +164 -0
  80. package/lib/session-split-group-v2-store.js +146 -0
  81. package/lib/session-split-groups.js +293 -133
  82. package/lib/sessions.js +89 -22
  83. package/lib/ws-schema.js +8 -0
  84. package/package.json +1 -1
@@ -0,0 +1,26 @@
1
+ var driverEligibility = require("./session-driver-eligibility");
2
+ var driverOrchestration = require("./session-driver-orchestration");
3
+ var prompts = require("./session-pair-prompts");
4
+
5
+ function attachSessionPairPrompt(ctx) {
6
+ function getSystemPrompt(session) {
7
+ var roles = ctx.rolesFor(session);
8
+ if (roles && roles.workers.indexOf(session) !== -1) {
9
+ var taskId = session._pairDelegation && session._pairDelegation.taskId || "unknown";
10
+ return prompts.worker(taskId);
11
+ }
12
+ var group = ctx.store.groupForMember(session.localId);
13
+ var result = "";
14
+ if (group && group.pair && group.pair.driverId === session.localId) {
15
+ result = prompts.DRIVER_CORE + " " + ctx.workerProposal().getSystemPrompt(session);
16
+ if (driverOrchestration.isHighTierDriverSession(session, ctx.sm)) result += " " + prompts.DRIVER_DELEGATION;
17
+ } else if (!group && !ctx.isMate && driverEligibility.isEligibleDriverSession(session, ctx.sm) &&
18
+ driverOrchestration.isHighTierDriverSession(session, ctx.sm)) {
19
+ result = prompts.UNPAIRED + " " + ctx.workerProposal().getSystemPrompt(session);
20
+ }
21
+ return result;
22
+ }
23
+ return { getSystemPrompt: getSystemPrompt };
24
+ }
25
+
26
+ module.exports = { attachSessionPairPrompt: attachSessionPairPrompt };
@@ -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 };
@@ -989,6 +989,8 @@ function attachSessions(ctx) {
989
989
  // TUI session: kill the underlying PTY before deleting the session
990
990
  // record so the `claude` process is reaped and not left orphaned.
991
991
  var dsTarget = sm.sessions.get(msg.id);
992
+ if (usersModule.isMultiUser() && (!ws._clayUser ||
993
+ !usersModule.canAccessSession(ws._clayUser.id, dsTarget, getProjectAccess()))) return true;
992
994
  if (dsTarget && dsTarget.mode === "tui" && typeof dsTarget.terminalId === "number" && tm) {
993
995
  try { tm.close(dsTarget.terminalId); } catch (e) {}
994
996
  }
@@ -1001,6 +1003,8 @@ function attachSessions(ctx) {
1001
1003
  if (msg.type === "rename_session") {
1002
1004
  if (msg.id && sm.sessions.has(msg.id) && msg.title) {
1003
1005
  var s = sm.sessions.get(msg.id);
1006
+ if (usersModule.isMultiUser() && (!ws._clayUser ||
1007
+ !usersModule.canAccessSession(ws._clayUser.id, s, getProjectAccess()))) return true;
1004
1008
  s.title = String(msg.title).substring(0, 100);
1005
1009
  s.titleProvisional = false;
1006
1010
  s.titleAutoGenerated = false;
@@ -1,5 +1,6 @@
1
1
  var path = require("path");
2
2
  var fs = require("fs");
3
+ var continuationLease = require("./driver-continuation-lease");
3
4
 
4
5
  /**
5
6
  * Attach user-message handler and remaining small handlers
@@ -398,6 +399,13 @@ function attachUserMessage(ctx) {
398
399
  if (pendingInternal && typeof pendingInternal.complete === "function") pendingInternal.complete(outcome, reason);
399
400
  }
400
401
 
402
+ var deliveryContext = messageDelivery.validateContext(ws, session, msg);
403
+ if (!deliveryContext.ok) {
404
+ sendTo(ws, { type: "error", text: deliveryContext.reason || "Message context changed." });
405
+ finishPendingEarly(false, deliveryContext.reason || "Message context changed");
406
+ return true;
407
+ }
408
+
401
409
  if (session.loopInterviewHandoff && session.loopInterviewHandoff.state === "starting") {
402
410
  sendTo(ws, { type: "error", text: "Wait for the approved Loop handoff to finish." });
403
411
  finishPendingEarly("release", "Loop handoff is still starting");
@@ -433,6 +441,19 @@ function attachUserMessage(ctx) {
433
441
  finishPendingEarly(false, "Pending messages cannot be delivered to a Driver-operated Split Worker");
434
442
  return true;
435
443
  }
444
+ if (!pendingInternal && session._driverContinuationLease && pendingMessageQueue) {
445
+ var continuationQueued = pendingMessageQueue.admit(session, msg, ws && ws._clayUser);
446
+ if (continuationQueued.ok) {
447
+ messageDelivery.acknowledge(ws, deliveryReceipt, null, session);
448
+ sendTo(ws, { type: "message_queued", clientMessageId: msg.clientMessageId || null,
449
+ sessionId: session.localId, projectSlug: slug, state: "pending",
450
+ paused: pendingMessageQueue.isPaused(session), revision: pendingMessageQueue.getRevision() });
451
+ continuationLease.cancelForHumanMessage(session);
452
+ } else {
453
+ sendTo(ws, { type: "error", text: continuationQueued.error || "Unable to queue message" });
454
+ }
455
+ return true;
456
+ }
436
457
  if (!pendingInternal && pendingMessageQueue && (pendingMessageQueue.hasActive(session) || session.isProcessing || session._queryStarting || session._awaitingTurnResult)) {
437
458
  var queuedResult = pendingMessageQueue.admit(session, msg, ws && ws._clayUser);
438
459
  if (queuedResult.ok) {