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
@@ -70,6 +70,8 @@ function attachPairResultOutbox(options) {
70
70
  }
71
71
  var nextRecords = Object.assign({}, records), next = Object.assign({}, record, {
72
72
  outcome: copy(outcome), state: record.state === "blocked" ? "blocked" : "captured", capturedAt: Date.now(),
73
+ preparedCloseOutcome: null,
74
+ closePreparationState: record.preparedCloseOutcome ? "superseded_by_terminal_evidence" : record.closePreparationState || null,
73
75
  });
74
76
  nextRecords[record.key] = next;
75
77
  var saved = persist(nextRecords);
@@ -87,6 +89,15 @@ function attachPairResultOutbox(options) {
87
89
  function retryCapture(key, operation, attempt) {
88
90
  attempt = attempt || 1;
89
91
  if (captureOperations[key] !== operation || !operation.active) return { ok: false, stale: true };
92
+ var isCurrent = true;
93
+ try { if (operation.isCurrent) isCurrent = operation.isCurrent() === true; } catch (error) { isCurrent = false; }
94
+ if (!isCurrent) {
95
+ operation.active = false;
96
+ var stale = { ok: false, stale: true, error: "pair result capture no longer matches the live Worker task" };
97
+ operation.lastResult = stale;
98
+ notifyCapture(operation, stale);
99
+ return stale;
100
+ }
90
101
  var result = captureRecord(key, operation.outcome);
91
102
  operation.lastResult = result;
92
103
  if (result.ok) {
@@ -118,6 +129,7 @@ function attachPairResultOutbox(options) {
118
129
  generation: args.generation == null ? null : args.generation,
119
130
  driverSessionId: args.driverSessionId == null ? null : args.driverSessionId,
120
131
  workerSessionId: args.workerSessionId == null ? null : args.workerSessionId,
132
+ groupId: typeof args.groupId === "string" ? args.groupId : null,
121
133
  workerSessionOriginId: args.workerOriginId,
122
134
  historyStartIndex: args.historyStartIndex == null ? null : args.historyStartIndex,
123
135
  message: args.message || "",
@@ -143,7 +155,7 @@ function attachPairResultOutbox(options) {
143
155
  records = nextRecords;
144
156
  return { ok: true, key: key, record: copy(record), duplicate: false };
145
157
  }
146
- function capture(args, outcome, callback) {
158
+ function capture(args, outcome, callback, isCurrent) {
147
159
  var key = typeof args === "string" ? args : resultKey(args);
148
160
  var record = records[key];
149
161
  if (!record) return { ok: false, error: "pair result outbox record was not found" };
@@ -158,11 +170,65 @@ function attachPairResultOutbox(options) {
158
170
  addCaptureCallback(operation, callback);
159
171
  return retryCapture(key, operation, 1);
160
172
  }
161
- operation = { outcome: copy(outcome), callbacks: [], active: true, complete: false, lastResult: null, timer: null };
173
+ operation = { outcome: copy(outcome), callbacks: [], active: true, complete: false, lastResult: null, timer: null,
174
+ isCurrent: typeof isCurrent === "function" ? isCurrent : null };
162
175
  captureOperations[key] = operation;
163
176
  addCaptureCallback(operation, callback);
164
177
  return retryCapture(key, operation, 1);
165
178
  }
179
+ function prepareCapture(key, outcome) {
180
+ var record = records[key];
181
+ if (!record) return { ok: false, error: "pair result outbox record was not found" };
182
+ if (record.outcome) {
183
+ if (!sameOutcome(record.outcome, outcome)) return { ok: false, conflict: true, error: "pair result outcome is immutable", record: copy(record) };
184
+ return { ok: true, record: copy(record), duplicate: true, committed: true };
185
+ }
186
+ if (record.preparedCloseOutcome) {
187
+ if (!sameOutcome(record.preparedCloseOutcome, outcome)) return { ok: false, conflict: true, error: "prepared close outcome changed", record: copy(record) };
188
+ return { ok: true, record: copy(record), duplicate: true, prepared: true };
189
+ }
190
+ var nextRecords = Object.assign({}, records), next = Object.assign({}, record, {
191
+ state: "close_prepared", preparedCloseOutcome: copy(outcome),
192
+ closePreparationState: "prepared", closePreparedAt: Date.now(),
193
+ });
194
+ nextRecords[key] = next;
195
+ var saved = persist(nextRecords);
196
+ if (!saved.ok) return { ok: false, error: saved.error, record: copy(record) };
197
+ records = nextRecords;
198
+ return { ok: true, record: copy(next), duplicate: false, prepared: true };
199
+ }
200
+ function finalizePreparedCapture(key, outcome) {
201
+ var record = records[key];
202
+ if (!record) return { ok: false, error: "pair result outbox record was not found" };
203
+ if (record.outcome) return captureRecord(key, outcome);
204
+ if (!record.preparedCloseOutcome || !sameOutcome(record.preparedCloseOutcome, outcome)) {
205
+ return { ok: false, error: "prepared close outcome is unavailable or changed", record: copy(record) };
206
+ }
207
+ var nextRecords = Object.assign({}, records), next = Object.assign({}, record, {
208
+ outcome: copy(outcome), state: "captured", capturedAt: Date.now(),
209
+ preparedCloseOutcome: null, closePreparationState: "committed", closeCommittedAt: Date.now(),
210
+ });
211
+ nextRecords[key] = next;
212
+ var saved = persist(nextRecords);
213
+ if (!saved.ok) return { ok: false, error: saved.error, record: copy(record) };
214
+ records = nextRecords;
215
+ return { ok: true, record: copy(next), duplicate: false };
216
+ }
217
+ function rollbackPreparedCapture(key, outcome) {
218
+ var record = records[key];
219
+ if (!record || !sameOutcome(record.preparedCloseOutcome, outcome) || record.outcome || record.finalized || record.transcriptPersisted ||
220
+ record.deliveryAttemptCount !== 0 || record.deliveryState !== "pending") {
221
+ return { ok: false, error: "prepared pair result can no longer be rolled back" };
222
+ }
223
+ var nextRecords = Object.assign({}, records), next = Object.assign({}, record, {
224
+ state: "dispatching", preparedCloseOutcome: null, closePreparationState: null, closePreparedAt: null,
225
+ });
226
+ nextRecords[key] = next;
227
+ var saved = persist(nextRecords);
228
+ if (!saved.ok) return { ok: false, error: saved.error, record: copy(record) };
229
+ records = nextRecords;
230
+ return { ok: true, record: copy(next) };
231
+ }
166
232
  function transition(key, changes) {
167
233
  var record = records[key];
168
234
  if (!record) return { ok: false, error: "pair result outbox record was not found" };
@@ -229,7 +295,7 @@ function attachPairResultOutbox(options) {
229
295
  function recoverPending(sessionMap) {
230
296
  var results = [], sessionsList = sessionMap && typeof sessionMap.values === "function" ? Array.from(sessionMap.values()) : [], keys = Object.keys(records);
231
297
  for (var i = 0; i < keys.length; i++) {
232
- if (records[keys[i]].state !== "dispatching") continue;
298
+ if (records[keys[i]].state !== "dispatching" && records[keys[i]].state !== "close_prepared") continue;
233
299
  var session = sessionsList.find(function (candidate) { return stableOrigin(candidate) === records[keys[i]].workerOriginId && candidate.ownerId === records[keys[i]].ownerId; });
234
300
  if (session) results.push({ key: keys[i], result: recover(keys[i], session) });
235
301
  }
@@ -294,7 +360,7 @@ function attachPairResultOutbox(options) {
294
360
  return Object.keys(source).map(function (key) {
295
361
  var record = copy(source[key]);
296
362
  record.deliveryState = record.deliveryState || "pending";
297
- record.recoveryState = record.state === "dispatching" || record.deliveryState === "attempting" || record.deliveryState === "uncertain" ? "uncertain" : "known";
363
+ record.recoveryState = record.state === "dispatching" || record.state === "close_prepared" || record.deliveryState === "attempting" || record.deliveryState === "uncertain" ? "uncertain" : "known";
298
364
  record.blockedReason = record.blockedReason || null;
299
365
  return record;
300
366
  });
@@ -306,6 +372,9 @@ function attachPairResultOutbox(options) {
306
372
  return {
307
373
  begin: begin,
308
374
  capture: capture,
375
+ prepareCapture: prepareCapture,
376
+ finalizePreparedCapture: finalizePreparedCapture,
377
+ rollbackPreparedCapture: rollbackPreparedCapture,
309
378
  beginDelivery: beginDelivery,
310
379
  markTranscript: markTranscript,
311
380
  markDelivery: markDelivery,
@@ -1,4 +1,5 @@
1
1
  var crypto = require("crypto");
2
+ var splitRoles = require("./session-split-group-roles");
2
3
 
3
4
  function attachPairResultRecovery(ctx) {
4
5
  var scheduled = Object.create(null);
@@ -23,8 +24,9 @@ function attachPairResultRecovery(ctx) {
23
24
  if (!driver || !worker) return { ok: false, code: "session", reason: "The original Driver or Split Worker session is unavailable." };
24
25
  if (ownerId(driver) !== (record.ownerId || null) || ownerId(worker) !== (record.ownerId || null)) return { ok: false, code: "owner", reason: "The original session owner no longer matches." };
25
26
  var group = ctx.store.groupForMember(driver.localId);
26
- if (!group || ctx.store.groupForMember(worker.localId) !== group || !group.pair ||
27
- group.pair.driverId !== driver.localId || group.pair.workerId !== worker.localId) {
27
+ var roles = splitRoles.resolveLiveRoles(ctx.sm, group);
28
+ if (!group || ctx.store.groupForMember(worker.localId) !== group || !roles ||
29
+ roles.driver !== driver || roles.workers.indexOf(worker) === -1) {
28
30
  return { ok: false, code: "pair", reason: "The original Driver and Split Worker pair is no longer active." };
29
31
  }
30
32
  var provenance = worker.sessionProvenance;
@@ -49,6 +51,7 @@ function attachPairResultRecovery(ctx) {
49
51
  }
50
52
  token = {
51
53
  outboxKey: record.key, taskId: record.taskId, generation: record.generation,
54
+ workerSessionId: resolved.worker.localId, workerId: resolved.worker.localId,
52
55
  from: resolved.driver.localId, groupId: resolved.group.id,
53
56
  startIndex: Number.isInteger(record.historyStartIndex) ? record.historyStartIndex : resolved.worker.history.length,
54
57
  message: record.message || "", wait: record.deliveryRoute === "blocking",
@@ -118,6 +121,7 @@ function attachPairResultRecovery(ctx) {
118
121
  return runtimeStatus[record.key] ? Object.assign({}, record, runtimeStatus[record.key]) : record;
119
122
  }).filter(function (record) {
120
123
  if (record.projectSlug !== ctx.projectSlug || record.driverOriginId !== originId || (record.ownerId || null) !== currentOwner) return false;
124
+ if (record.state === "close_prepared") return false;
121
125
  if (healthyLiveDispatch(record)) return false;
122
126
  if (record.finalized) return false;
123
127
  if (record.deliveryState === "accepted") return record.state === "blocked" || record.finalizationState === "failed";
@@ -0,0 +1,20 @@
1
+ var splitRoles = require("./session-split-group-roles");
2
+
3
+ function removeSelectedWorker(store, group, driver, worker, capability, hooks) {
4
+ var normalized = splitRoles.normalizePair(group.pair, group.members);
5
+ if (!normalized.ok || normalized.kind !== "versioned") {
6
+ var ws = { _clayUser: driver.ownerId ? { id: driver.ownerId } : null };
7
+ return store.dissolve(ws, { id: group.id }, hooks);
8
+ }
9
+ return store.removeWorker(driver.ownerId || null, {
10
+ groupId: group.id,
11
+ driverId: driver.localId,
12
+ driverOriginId: driver.sessionOriginId || null,
13
+ workerId: worker.localId,
14
+ workerOriginId: worker.sessionOriginId || null,
15
+ generation: worker._pairGeneration ||
16
+ worker.sessionProvenance && worker.sessionProvenance.generation || null,
17
+ }, capability, hooks);
18
+ }
19
+
20
+ module.exports = { removeSelectedWorker: removeSelectedWorker };
@@ -43,6 +43,9 @@ function attachPairTaskControl(ctx) {
43
43
  function taskGeneration(worker) {
44
44
  return worker._pairGeneration || worker.sessionProvenance && worker.sessionProvenance.generation || null;
45
45
  }
46
+ function matchesGeneration(worker, task) {
47
+ return !!(worker && task && task.workerSessionId === worker.localId && task.generation === taskGeneration(worker));
48
+ }
46
49
  function findTask(worker, taskId) {
47
50
  var active = current(worker);
48
51
  if (active && active.taskId === taskId) return active;
@@ -65,6 +68,7 @@ function attachPairTaskControl(ctx) {
65
68
  taskId: taskId,
66
69
  generation: taskGeneration(worker),
67
70
  workerSessionId: worker.localId,
71
+ workerId: worker.localId,
68
72
  driverSessionId: caller.localId,
69
73
  message: message,
70
74
  autonomousRunId: caller.autonomousRun && caller.autonomousRun.id || null,
@@ -89,7 +93,7 @@ function attachPairTaskControl(ctx) {
89
93
  function queue(args, caller) {
90
94
  try {
91
95
  ctx.turnControl.assertWorkerAction(caller);
92
- var resolved = ctx.resolvePair(caller);
96
+ var resolved = ctx.resolvePair(caller, args, { mutation: true });
93
97
  var worker = resolved.partner;
94
98
  var message = cleanText(args.message, 30000);
95
99
  if (!message) throw new Error("message is required");
@@ -104,14 +108,14 @@ function attachPairTaskControl(ctx) {
104
108
  }
105
109
  function inspect(args, caller) {
106
110
  try {
107
- var worker = ctx.resolvePair(caller).partner;
111
+ var worker = ctx.resolvePair(caller, args, { readOnly: true }).partner;
108
112
  return result(status(worker));
109
113
  } catch (err) { return result({ status: "rejected", reason: err.message || String(err) }); }
110
114
  }
111
115
  function cancel(args, caller) {
112
116
  try {
113
117
  ctx.turnControl.assertWorkerAction(caller);
114
- var worker = ctx.resolvePair(caller).partner;
118
+ var worker = ctx.resolvePair(caller, args, { mutation: true }).partner;
115
119
  var task = findTask(worker, args.taskId);
116
120
  if (!task || (task.status !== "queued" && task.status !== "waiting_for_interrupt")) throw new Error("the exact queued task is not cancellable");
117
121
  task.status = "cancelled";
@@ -150,7 +154,7 @@ function attachPairTaskControl(ctx) {
150
154
  function replaceTask(args, caller) {
151
155
  try {
152
156
  ctx.turnControl.assertWorkerAction(caller);
153
- var resolved = ctx.resolvePair(caller);
157
+ var resolved = ctx.resolvePair(caller, args, { mutation: true });
154
158
  var worker = resolved.partner;
155
159
  var active = current(worker);
156
160
  if (!active || !worker.isProcessing) throw new Error("there is no active delegated task to replace");
@@ -170,12 +174,12 @@ function attachPairTaskControl(ctx) {
170
174
  function resumeTask(args, caller) {
171
175
  try {
172
176
  ctx.turnControl.assertWorkerAction(caller);
173
- var worker = ctx.resolvePair(caller).partner;
177
+ var worker = ctx.resolvePair(caller, args, { mutation: true }).partner;
174
178
  if (worker.isProcessing || worker._queryStarting || current(worker)) throw new Error("the Split Worker is not idle");
175
179
  var prior = worker._lastInterruptedPairTask;
176
180
  if (!prior) throw new Error("the Split Worker has no resumable interrupted task");
177
181
  var message = cleanText(args.message, 30000) || prior.message;
178
- return ctx.sendFollowup({ message: message, wait: args.wait, timeoutSeconds: args.timeoutSeconds, taskId: args.taskId }, caller);
182
+ return ctx.sendFollowup({ message: message, wait: args.wait, timeoutSeconds: args.timeoutSeconds, taskId: args.taskId, workerId: worker.localId }, caller);
179
183
  } catch (err) { return result({ status: "rejected", reason: err.message || String(err) }); }
180
184
  }
181
185
  function report(args, worker) {
@@ -185,6 +189,7 @@ function attachPairTaskControl(ctx) {
185
189
  if (!resolved || resolved.worker !== worker) throw new Error("only the exact configured Split Worker can report an outcome");
186
190
  var active = current(worker);
187
191
  if (!active || args.taskId !== active.taskId) throw new Error("taskId must match the exact active delegated task");
192
+ if (!matchesGeneration(worker, active)) throw new Error("the active task no longer matches this Worker generation");
188
193
  var outcome = typeof args.outcome === "string" ? args.outcome.trim().toLowerCase() : "";
189
194
  if (OUTCOMES.indexOf(outcome) === -1) throw new Error("outcome must be completed, partial, failed, or interrupted");
190
195
  active.reportedOutcome = {
@@ -200,11 +205,12 @@ function attachPairTaskControl(ctx) {
200
205
  return result({ status: "recorded", taskId: active.taskId });
201
206
  } catch (err) { return result({ status: "rejected", reason: err.message || String(err) }); }
202
207
  }
203
- function complete(worker, caller, token, observedStatus, response, error) {
208
+ function prepareCompletion(worker, caller, token, observedStatus, response, error, forcedInterruption) {
204
209
  if (!token) return null;
205
210
  if (token.outcomeEnvelope) return token.outcomeEnvelope;
206
- var interruption = worker._pairInterruption && worker._pairInterruption.targetTaskId === token.taskId ? worker._pairInterruption : null;
207
- if (interruption) interruption.completedAt = Date.now();
211
+ var currentInterruption = forcedInterruption ||
212
+ (worker._pairInterruption && worker._pairInterruption.targetTaskId === token.taskId ? worker._pairInterruption : null);
213
+ var interruption = currentInterruption ? Object.assign({}, currentInterruption, { completedAt: Date.now() }) : null;
208
214
  var report = token.reportedOutcome || null;
209
215
  var finalStatus = error ? "failed" : (interruption ? "interrupted" : (report ? report.outcome : observedStatus));
210
216
  var envelope = {
@@ -224,10 +230,16 @@ function attachPairTaskControl(ctx) {
224
230
  response: response || "",
225
231
  error: error || null,
226
232
  };
227
- token.status = finalStatus;
233
+ return envelope;
234
+ }
235
+ function commitCompletion(worker, caller, token, envelope) {
236
+ if (!token || !envelope) return envelope || null;
237
+ if (token.outcomeEnvelope) return token.outcomeEnvelope;
238
+ token.status = envelope.status;
228
239
  token.outcomeEnvelope = envelope;
229
240
  worker._lastPairOutcome = envelope;
230
- if (finalStatus === "interrupted" || finalStatus === "partial") worker._lastInterruptedPairTask = { taskId: token.taskId, message: token.message, generation: token.generation };
241
+ if (envelope.interruption) worker._pairInterruption = envelope.interruption;
242
+ if (envelope.status === "interrupted" || envelope.status === "partial") worker._lastInterruptedPairTask = { taskId: token.taskId, message: token.message, generation: token.generation };
231
243
  if (ctx.sm.sendAndRecord) {
232
244
  try { ctx.sm.sendAndRecord(worker, { type: "pair_task_completed", _internal: true,
233
245
  ownerId: caller.ownerId || null, driverOriginId: caller.sessionOriginId || null,
@@ -237,6 +249,10 @@ function attachPairTaskControl(ctx) {
237
249
  if (ctx.sm.sendToSession) ctx.sm.sendToSession(caller, { type: "partner_task_completed", outcome: envelope });
238
250
  return envelope;
239
251
  }
252
+ function complete(worker, caller, token, observedStatus, response, error) {
253
+ return commitCompletion(worker, caller, token,
254
+ prepareCompletion(worker, caller, token, observedStatus, response, error));
255
+ }
240
256
  function status(worker) {
241
257
  var active = current(worker);
242
258
  var registry = registryFor(worker);
@@ -261,23 +277,28 @@ function attachPairTaskControl(ctx) {
261
277
  next.status = "starting";
262
278
  next.startedAt = Date.now();
263
279
  queue.splice(queue.indexOf(next), 1);
264
- Promise.resolve(ctx.sendFollowup({ message: next.message, wait: false, taskId: next.taskId }, caller)).then(function (delivery) {
280
+ Promise.resolve(ctx.sendFollowup({ message: next.message, wait: false, taskId: next.taskId, workerId: next.workerId }, caller)).then(function (delivery) {
265
281
  next.status = delivery && delivery.isError ? "failed" : "running";
266
282
  if (delivery && delivery.isError) next.error = delivery.content && delivery.content[0] && delivery.content[0].text || "Follow-up rejected";
267
283
  }, function (err) { next.status = "failed"; next.error = err.message || String(err); });
268
284
  return true;
269
285
  }
270
286
  function driverToolDefs(handlers) {
271
- return [{ name: "queue_partner_followup", description: "Queue a distinct follow-up task behind the active Split Worker task.", inputSchema: buildShape({ message: { type: "string" }, taskId: { type: "string" } }, ["message"]), handler: handlers.queue },
272
- { name: "inspect_partner_followups", description: "Inspect current, queued, interrupted, and last completed Split Worker task state.", inputSchema: buildShape({}), handler: handlers.inspect },
273
- { name: "cancel_partner_followup", description: "Cancel one exact queued follow-up without interrupting active work.", inputSchema: buildShape({ taskId: { type: "string" } }, ["taskId"]), handler: handlers.cancel },
274
- { name: "replace_partner_task", description: "Interrupt one exact active delegated task and queue a replacement task in the same Worker generation.", inputSchema: buildShape({ targetTaskId: { type: "string" }, message: { type: "string" }, taskId: { type: "string" }, reason: { type: "string" } }, ["targetTaskId", "message"]), handler: handlers.replace },
275
- { name: "resume_partner_task", description: "Resume the last interrupted task as a new correlated task. A human Stop blocks this until a new user turn.", inputSchema: buildShape({ message: { type: "string" }, taskId: { type: "string" }, wait: { type: "boolean" }, timeoutSeconds: { type: "number" } }), handler: handlers.resume }];
287
+ var workerId = { type: "number", description: "Optional exact Worker session id; required when multiple Workers are configured." };
288
+ return [{ name: "queue_partner_followup", description: "Queue a distinct follow-up task behind the active Split Worker task.", inputSchema: buildShape({ message: { type: "string" }, taskId: { type: "string" }, workerId: workerId }, ["message"]), handler: handlers.queue },
289
+ { name: "inspect_partner_followups", description: "Inspect current, queued, interrupted, and last completed Split Worker task state.", inputSchema: buildShape({ workerId: workerId }), handler: handlers.inspect },
290
+ { name: "cancel_partner_followup", description: "Cancel one exact queued follow-up without interrupting active work.", inputSchema: buildShape({ taskId: { type: "string" }, workerId: workerId }, ["taskId"]), handler: handlers.cancel },
291
+ { name: "replace_partner_task", description: "Interrupt one exact active delegated task and queue a replacement task in the same Worker generation.", inputSchema: buildShape({ targetTaskId: { type: "string" }, message: { type: "string" }, taskId: { type: "string" }, reason: { type: "string" }, workerId: workerId }, ["targetTaskId", "message"]), handler: handlers.replace },
292
+ { name: "resume_partner_task", description: "Resume the last interrupted task as a new correlated task. A human Stop blocks this until a new user turn.", inputSchema: buildShape({ message: { type: "string" }, taskId: { type: "string" }, wait: { type: "boolean" }, timeoutSeconds: { type: "number" }, workerId: workerId }), handler: handlers.resume }];
276
293
  }
277
294
  function workerToolDefs(worker) {
278
295
  return [{ name: "report_partner_outcome", description: "Report structured, Worker-observed task outcome data. This does not claim Driver verification.", inputSchema: buildShape({ taskId: { type: "string" }, outcome: { type: "string" }, summary: { type: "string" }, changedFiles: { type: "array", items: { type: "string" } }, verifications: { type: "array", items: { type: "object" } }, unverified: { type: "array", items: { type: "string" } }, processes: { type: "array", items: { type: "string" } }, nextAction: { type: "string" } }, ["taskId", "outcome"]), handler: function (args) { return report(args || {}, worker); } }];
279
296
  }
280
- return { begin: begin, cancel: cancel, cancelAll: cancelAll, complete: complete, drain: drain, driverToolDefs: driverToolDefs, ensureSystemInterruption: ensureSystemInterruption, inspect: inspect, markInterruption: markInterruption, queue: queue, replaceTask: replaceTask, resumeTask: resumeTask, status: status, workerToolDefs: workerToolDefs };
297
+ return { begin: begin, cancel: cancel, cancelAll: cancelAll, commitCompletion: commitCompletion, complete: complete,
298
+ prepareCompletion: prepareCompletion, drain: drain, driverToolDefs: driverToolDefs,
299
+ ensureSystemInterruption: ensureSystemInterruption, inspect: inspect, markInterruption: markInterruption,
300
+ matchesGeneration: matchesGeneration, queue: queue, replaceTask: replaceTask,
301
+ resumeTask: resumeTask, status: status, workerToolDefs: workerToolDefs };
281
302
  }
282
303
 
283
304
  module.exports = { attachPairTaskControl: attachPairTaskControl };
@@ -0,0 +1,68 @@
1
+ var multiWorkerFeature = require("./multi-worker-feature");
2
+ var splitRoles = require("./session-split-group-roles");
3
+ var generationOf = require("./session-split-group-v2-store").generationOf;
4
+
5
+ function attachPairWorkerClose(ctx) {
6
+ function result(ws, msg, ok, error) {
7
+ ctx.sendTo(ws, {
8
+ type: "split_worker_close_result",
9
+ requestId: typeof msg.requestId === "string" ? msg.requestId : null,
10
+ ok: ok,
11
+ error: error || null,
12
+ groupId: typeof msg.groupId === "string" ? msg.groupId : null,
13
+ workerId: Number.isInteger(msg.workerId) ? msg.workerId : null,
14
+ });
15
+ }
16
+
17
+ function validate(ws, msg) {
18
+ if (ctx.isMate || !multiWorkerFeature.isEnabled(ctx.multiWorkerFeature)) {
19
+ throw new Error("Multi-Worker close is unavailable in this project");
20
+ }
21
+ if (!ctx.hasSocket(ws) || ws.readyState !== 1) throw new Error("The project socket is no longer active");
22
+ if (msg.projectSlug !== ctx.projectSlug) throw new Error("The project changed before the Worker was closed");
23
+ if (!Number.isInteger(msg.driverId) || ws._clayActiveSession !== msg.driverId) {
24
+ throw new Error("The active session must be the configured Driver");
25
+ }
26
+ var driver = ctx.sm.sessions.get(msg.driverId);
27
+ if (!driver || ctx.sm.sessions.get(driver.localId) !== driver) throw new Error("The exact Driver session is no longer live");
28
+ var actorId = ws._clayUser && ws._clayUser.id || null;
29
+ if ((driver.ownerId || null) !== actorId) throw new Error("The Driver owner does not match this socket");
30
+ if ((msg.driverOriginId || null) !== (driver.sessionOriginId || null)) throw new Error("The exact Driver identity changed");
31
+ var group = ctx.splitStore.groupForMember(driver.localId);
32
+ if (!group || group.id !== msg.groupId || (group.ownerId || null) !== actorId) throw new Error("The exact split group changed");
33
+ var roles = splitRoles.normalizePair(group.pair, group.members);
34
+ if (!roles.ok || roles.kind !== "versioned" || roles.driverId !== driver.localId) {
35
+ throw new Error("The configured Driver or Worker roles changed");
36
+ }
37
+ if (!Array.isArray(msg.expectedWorkerIds) || msg.expectedWorkerIds.join(",") !== roles.workerIds.join(",")) {
38
+ throw new Error("The configured Worker membership changed");
39
+ }
40
+ if (!Number.isInteger(msg.workerId) || roles.workerIds.indexOf(msg.workerId) === -1) {
41
+ throw new Error("The selected Worker changed");
42
+ }
43
+ var worker = ctx.sm.sessions.get(msg.workerId);
44
+ if (!worker || ctx.sm.sessions.get(worker.localId) !== worker || (worker.ownerId || null) !== actorId) {
45
+ throw new Error("The exact Worker session is no longer live");
46
+ }
47
+ if ((msg.workerOriginId || null) !== (worker.sessionOriginId || null) || msg.generation !== generationOf(worker)) {
48
+ throw new Error("The exact Worker identity or generation changed");
49
+ }
50
+ return { driver: driver, worker: worker };
51
+ }
52
+
53
+ function handleMessage(ws, msg) {
54
+ if (!msg || msg.type !== "split_worker_close") return false;
55
+ try {
56
+ var exact = validate(ws, msg);
57
+ ctx.closeWorker({ workerId: exact.worker.localId }, exact.driver);
58
+ result(ws, msg, true, null);
59
+ } catch (error) {
60
+ result(ws, msg, false, error.message || String(error));
61
+ }
62
+ return true;
63
+ }
64
+
65
+ return { handleMessage: handleMessage, validate: validate };
66
+ }
67
+
68
+ module.exports = { attachPairWorkerClose: attachPairWorkerClose };
@@ -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 };