clay-server 4.3.0-beta.5 → 4.3.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/lib/driver-continuation-pair.js +5 -1
  2. package/lib/multi-worker-feature.js +11 -0
  3. package/lib/project-pair-autonomous-stop.js +14 -8
  4. package/lib/project-pair-close-transaction.js +59 -0
  5. package/lib/project-pair-global-stop.js +18 -0
  6. package/lib/project-pair-lifecycle-status.js +34 -0
  7. package/lib/project-pair-lifecycle.js +58 -76
  8. package/lib/project-pair-message.js +1 -1
  9. package/lib/project-pair-owned-stop.js +7 -4
  10. package/lib/project-pair-result-capture.js +41 -11
  11. package/lib/project-pair-result-delivery.js +4 -3
  12. package/lib/project-pair-result-outbox.js +73 -4
  13. package/lib/project-pair-result-recovery.js +6 -2
  14. package/lib/project-pair-structural-close.js +20 -0
  15. package/lib/project-pair-task-control.js +39 -18
  16. package/lib/project-pair-worker-close.js +68 -0
  17. package/lib/project-session-pair-prompt.js +26 -0
  18. package/lib/project-session-pair.js +55 -78
  19. package/lib/project-worker-creation-proposal.js +176 -0
  20. package/lib/project-worker-permission.js +7 -8
  21. package/lib/project-worker-proposal-runner.js +45 -0
  22. package/lib/project-worker-proposal.js +79 -115
  23. package/lib/project.js +15 -0
  24. package/lib/public/css/pane.css +48 -2
  25. package/lib/public/modules/app-connection.js +49 -76
  26. package/lib/public/modules/app-messages.js +4 -0
  27. package/lib/public/modules/project-activation.js +10 -7
  28. package/lib/public/modules/sidebar-sessions.js +31 -11
  29. package/lib/public/modules/split-group-helpers.js +74 -2
  30. package/lib/public/modules/split-pair-ui.js +28 -6
  31. package/lib/public/modules/split-pane-reconciler.js +28 -0
  32. package/lib/public/modules/split-pane-renderer.js +107 -0
  33. package/lib/public/modules/split-view.js +58 -188
  34. package/lib/public/modules/websocket-watchdog.js +126 -0
  35. package/lib/public/modules/worker-pane-lock.js +13 -3
  36. package/lib/server.js +1 -0
  37. package/lib/session-pair-factory.js +66 -0
  38. package/lib/session-pair-mcp-server.js +8 -3
  39. package/lib/session-pair-prompts.js +34 -24
  40. package/lib/session-pair-target.js +42 -0
  41. package/lib/session-pair-turn-control.js +11 -6
  42. package/lib/session-split-group-anchors.js +89 -7
  43. package/lib/session-split-group-persistence.js +136 -0
  44. package/lib/session-split-group-roles.js +164 -0
  45. package/lib/session-split-group-v2-store.js +146 -0
  46. package/lib/session-split-groups.js +109 -99
  47. package/lib/ws-schema.js +2 -0
  48. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  var sessionProvenance = require("./session-provenance");
2
+ var splitRoles = require("./session-split-group-roles");
2
3
 
3
4
  var ACTIVE_RUN_STATES = ["armed", "running", "reviewing", "waiting-worker", "waiting-user", "paused"];
4
5
 
@@ -37,7 +38,10 @@ function inspect(source, ctx) {
37
38
  if (!group.pair || group.pair.driverId !== source.localId) {
38
39
  return { ok: false, error: "Only the configured Driver can continue an attached split pair." };
39
40
  }
40
- var worker = ctx.sm && ctx.sm.sessions.get(group.pair.workerId);
41
+ var roles = splitRoles.normalizePair(group.pair, group.members);
42
+ if (!roles.ok || roles.kind === "adhoc") return { ok: false, error: "The attached split pair roles are invalid." };
43
+ if (roles.kind === "versioned") return { ok: false, error: "Driver continuation does not yet support a multi-Worker group." };
44
+ var worker = ctx.sm && ctx.sm.sessions.get(roles.workerIds[0]);
41
45
  if (!worker || group.members.indexOf(worker.localId) === -1) return { ok: false, error: "The attached Split Worker is unavailable." };
42
46
  if ((source.ownerId || null) !== (worker.ownerId || null)) return { ok: false, error: "The attached Split Worker owner changed." };
43
47
  if (!sessionProvenance.isWorker(worker)) return { ok: false, error: "The attached split partner is not a verified Worker." };
@@ -0,0 +1,11 @@
1
+ var ENABLED_GATE = Object.freeze({ feature: "multi-worker-runtime" });
2
+
3
+ function fromServerConfig(config) {
4
+ return config && config.multiWorkerRuntimeEnabled === true ? ENABLED_GATE : null;
5
+ }
6
+
7
+ function isEnabled(gate) {
8
+ return gate === ENABLED_GATE;
9
+ }
10
+
11
+ module.exports = { fromServerConfig: fromServerConfig, isEnabled: isEnabled };
@@ -1,15 +1,21 @@
1
1
  function stopAutonomousWork(session, roles, turnControl, taskControl, workerPermission) {
2
2
  if (!roles) return false;
3
3
  var runId = session.autonomousRun && session.autonomousRun.id;
4
- var cancelled = taskControl.cancelAll(roles.worker, runId);
5
- var owned = roles.worker._pairDelegation && roles.worker._pairDelegation.autonomousRunId === runId;
6
- if (!owned && !cancelled) return true;
4
+ var workers = roles.workers || [roles.worker];
5
+ var cancelled = 0, owned = [];
6
+ for (var wi = 0; wi < workers.length; wi++) {
7
+ cancelled += taskControl.cancelAll(workers[wi], runId);
8
+ if (workers[wi]._pairDelegation && workers[wi]._pairDelegation.autonomousRunId === runId) owned.push(workers[wi]);
9
+ }
10
+ if (!owned.length && !cancelled) return true;
7
11
  turnControl.markHumanStop(session);
8
- if (!owned) return true;
9
- taskControl.markInterruption(roles.worker, "user", "The Until complete run was stopped.");
10
- workerPermission.cancelForSession(roles.worker, "The Until complete run was stopped.");
11
- roles.worker.taskStopRequested = true;
12
- if (roles.worker.abortController) roles.worker.abortController.abort();
12
+ if (!owned.length) return true;
13
+ for (var oi = 0; oi < owned.length; oi++) {
14
+ taskControl.markInterruption(owned[oi], "user", "The Until complete run was stopped.");
15
+ workerPermission.cancelForSession(owned[oi], "The Until complete run was stopped.");
16
+ owned[oi].taskStopRequested = true;
17
+ if (owned[oi].abortController) owned[oi].abortController.abort();
18
+ }
13
19
  return true;
14
20
  }
15
21
 
@@ -0,0 +1,59 @@
1
+ var structuralClose = require("./project-pair-structural-close");
2
+
3
+ function attachPairCloseTransaction(ctx) {
4
+ function close(args, caller) {
5
+ var resolved = ctx.resolvePair(caller, args);
6
+ var partner = resolved.partner;
7
+ var token = partner._pairDelegation || null;
8
+ var interrupted = !!(partner.isProcessing || partner._queryStarting);
9
+ var interruption = token && interrupted ? {
10
+ source: "driver",
11
+ reason: "The Driver closed the Split Worker pair.",
12
+ targetTaskId: token.taskId,
13
+ requestedAt: Date.now(),
14
+ } : null;
15
+ var response = token ? ctx.responseText(partner.history || [], token.startIndex) : "";
16
+ var failure = token ? ctx.errorSince(partner.history || [], token.startIndex) : null;
17
+ var outcome = token ? ctx.taskControl.prepareCompletion(partner, caller, token,
18
+ interrupted ? "interrupted" : "completed", response, failure, interruption) : null;
19
+ var prepared = token ? ctx.resultCapture.prepare(caller, partner, token, outcome) : { ok: true, prepared: false };
20
+ if (!prepared.ok) throw new Error(prepared.error || "could not durably capture the Worker outcome");
21
+ var postCommitError = null;
22
+ var result = structuralClose.removeSelectedWorker(ctx.store, resolved.group, caller, partner,
23
+ ctx.multiWorkerFeature, { afterPersist: function () {
24
+ try {
25
+ if (token) {
26
+ var finalized = ctx.resultCapture.finalizePrepared(token, outcome);
27
+ if (!finalized.ok) throw new Error(finalized.error || "prepared close outcome could not be committed");
28
+ partner._pairClosing = true;
29
+ token.response = response;
30
+ token.failure = failure;
31
+ token.interrupted = interrupted && !failure;
32
+ ctx.taskControl.commitCompletion(partner, caller, token, outcome);
33
+ ctx.resultCapture.acceptPrepared(caller, partner, token);
34
+ }
35
+ } catch (error) { postCommitError = error; }
36
+ finally { delete partner._pairClosing; }
37
+ return { ok: true };
38
+ } });
39
+ if (!result.ok) {
40
+ if (token && prepared.prepared && !prepared.duplicate) {
41
+ var rolledBack = ctx.resultCapture.rollbackPrepared(token, outcome);
42
+ if (!rolledBack.ok) throw new Error((result.error || "could not close the Worker pair") +
43
+ "; prepared result rollback failed: " + (rolledBack.error || "unknown error"));
44
+ }
45
+ throw new Error(result.error || "could not close the Worker pair");
46
+ }
47
+ if (interrupted) {
48
+ partner.taskStopRequested = true;
49
+ if (partner.abortController) partner.abortController.abort();
50
+ }
51
+ ctx.workerPermission.cancelForSession(partner, "The Driver closed the Split Worker pair.");
52
+ if (postCommitError) partner._pairCloseFinalizationError = postCommitError.message || String(postCommitError);
53
+ return { status: "closed", partnerId: partner.localId, interrupted: interrupted,
54
+ historyPreserved: true };
55
+ }
56
+ return { close: close };
57
+ }
58
+
59
+ module.exports = { attachPairCloseTransaction: attachPairCloseTransaction };
@@ -0,0 +1,18 @@
1
+ function stopAllWorkers(session, roles, ctx) {
2
+ if (!roles) return false;
3
+ for (var i = 0; i < roles.workers.length; i++) {
4
+ var worker = roles.workers[i];
5
+ if (worker._pairDelegation && worker._pairDelegation.outboxKey && ctx.resultOutbox) {
6
+ ctx.resultOutbox.markBlocked(worker._pairDelegation.outboxKey, "The human stopped this Split Worker turn.", "human_stop");
7
+ }
8
+ ctx.taskControl.markInterruption(worker, "user", "The human stopped this Split Worker turn.");
9
+ ctx.workerPermission.cancelForSession(worker, "The human stopped this Split Worker turn.");
10
+ worker.taskStopRequested = true;
11
+ if (worker.abortController) {
12
+ try { worker.abortController.abort(); } catch (e) {}
13
+ }
14
+ }
15
+ return true;
16
+ }
17
+
18
+ module.exports = { stopAllWorkers: stopAllWorkers };
@@ -0,0 +1,34 @@
1
+ var pairUsage = require("./project-pair-usage");
2
+
3
+ var MAX_TASK_PREVIEW_CHARS = 200;
4
+
5
+ function clampText(value, max) {
6
+ var text = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
7
+ if (text.length > max) text = text.slice(0, max - 1) + "…";
8
+ return text;
9
+ }
10
+
11
+ function continuityStatus(session) {
12
+ var history = session.history || [];
13
+ var userTurns = 0;
14
+ var errors = 0;
15
+ for (var i = 0; i < history.length; i++) {
16
+ if (!history[i]) continue;
17
+ if (history[i].type === "user_message") userTurns++;
18
+ else if (history[i].type === "error") errors++;
19
+ }
20
+ var lastActivity = typeof session.lastActivity === "number" ? session.lastActivity : null;
21
+ return { historyEntries: history.length, userTurns: userTurns, errorEntries: errors,
22
+ idleSeconds: lastActivity ? Math.max(0, Math.round((Date.now() - lastActivity) / 1000)) : null };
23
+ }
24
+
25
+ function activityStatus(session) {
26
+ var token = session._pairDelegation || null;
27
+ return { isProcessing: !!(session.isProcessing || session._queryStarting), delegated: !!token,
28
+ currentTask: token ? clampText(token.message, MAX_TASK_PREVIEW_CHARS) : "",
29
+ currentTaskId: token && token.taskId || null, interruption: session._pairInterruption || null,
30
+ lastTurnInterrupted: !!session._lastTurnInterrupted };
31
+ }
32
+
33
+ module.exports = { activityStatus: activityStatus, clampText: clampText, continuityStatus: continuityStatus,
34
+ contextStatus: pairUsage.contextStatus, firstNumber: pairUsage.firstNumber };
@@ -1,12 +1,16 @@
1
1
  var eligibility = require("./session-driver-eligibility");
2
2
  var replacementState = require("./project-pair-replacement-state");
3
- var pairUsage = require("./project-pair-usage");
4
- var contextStatus = pairUsage.contextStatus;
5
- var firstNumber = pairUsage.firstNumber;
3
+ var lifecycleStatus = require("./project-pair-lifecycle-status");
4
+ var contextStatus = lifecycleStatus.contextStatus;
5
+ var firstNumber = lifecycleStatus.firstNumber;
6
+ var continuityStatus = lifecycleStatus.continuityStatus;
7
+ var activityStatus = lifecycleStatus.activityStatus;
8
+ var clampText = lifecycleStatus.clampText;
9
+ var pairTarget = require("./session-pair-target");
10
+ var splitRoles = require("./session-split-group-roles");
6
11
 
7
12
  var MAX_GENERATIONS = 5;
8
13
  var MAX_NOTE_CHARS = 400;
9
- var MAX_TASK_PREVIEW_CHARS = 200;
10
14
 
11
15
  var EVALUATION_OUTCOMES = ["succeeded", "partial", "failed", "abandoned"];
12
16
 
@@ -21,57 +25,23 @@ function toolError(message) {
21
25
  });
22
26
  }
23
27
 
24
- function clampText(value, max) {
25
- var text = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
26
- if (text.length > max) text = text.slice(0, max - 1) + "…";
27
- return text;
28
- }
29
-
30
28
  function replacementFingerprint(args, worker) {
31
29
  return JSON.stringify({ sourceWorkerId: worker.localId, interrupt: args.interrupt === true,
32
30
  vendor: args.workerVendor || "", model: args.workerModel || "", effort: args.workerEffort || "",
33
31
  message: args.message || "", evaluation: args.evaluation || null });
34
32
  }
35
33
 
36
- function continuityStatus(session) {
37
- var history = session.history || [];
38
- var userTurns = 0;
39
- var errors = 0;
40
- for (var i = 0; i < history.length; i++) {
41
- if (!history[i]) continue;
42
- if (history[i].type === "user_message") userTurns++;
43
- else if (history[i].type === "error") errors++;
44
- }
45
- var lastActivity = typeof session.lastActivity === "number" ? session.lastActivity : null;
46
- return {
47
- historyEntries: history.length,
48
- userTurns: userTurns,
49
- errorEntries: errors,
50
- idleSeconds: lastActivity ? Math.max(0, Math.round((Date.now() - lastActivity) / 1000)) : null,
51
- };
52
- }
53
-
54
- function activityStatus(session) {
55
- var token = session._pairDelegation || null;
56
- return {
57
- isProcessing: !!(session.isProcessing || session._queryStarting),
58
- delegated: !!token,
59
- currentTask: token ? clampText(token.message, MAX_TASK_PREVIEW_CHARS) : "",
60
- currentTaskId: token && token.taskId || null,
61
- interruption: session._pairInterruption || null,
62
- lastTurnInterrupted: !!session._lastTurnInterrupted,
63
- };
64
- }
65
-
66
34
  function attachPairLifecycle(ctx) {
67
35
  var sm = ctx.sm;
68
36
  var store = ctx.splitStore;
69
37
  var turnControl = ctx.turnControl;
70
38
 
71
- function ledgerFor(driver) {
39
+ function ledgerFor(driver, selectedWorker) {
72
40
  if (!Array.isArray(driver._workerGenerations)) driver._workerGenerations = [];
73
41
  var group = store.groupForMember(driver.localId);
74
- var worker = group && group.pair && sm.sessions.get(group.pair.workerId);
42
+ var normalized = splitRoles.normalizePair(group && group.pair, group && group.members);
43
+ var fallbackId = normalized.ok && normalized.kind !== "adhoc" && normalized.workerIds.length === 1 ? normalized.workerIds[0] : null;
44
+ var worker = selectedWorker || fallbackId != null && sm.sessions.get(fallbackId);
75
45
  var generation = worker && (worker._pairGeneration || worker.sessionProvenance && worker.sessionProvenance.generation);
76
46
  if (worker && Number.isInteger(generation) && generation > 0 && !findGeneration(driver, worker)) {
77
47
  driver._workerGenerations.push({
@@ -91,7 +61,7 @@ function attachPairLifecycle(ctx) {
91
61
  return driver._workerGenerations;
92
62
  }
93
63
  function recordGenerationStart(driver, worker) {
94
- var ledger = ledgerFor(driver);
64
+ var ledger = ledgerFor(driver, worker);
95
65
  var existing = findGeneration(driver, worker);
96
66
  if (existing && !existing.endedAt) {
97
67
  worker._pairGeneration = existing.generation;
@@ -166,6 +136,8 @@ function attachPairLifecycle(ctx) {
166
136
  return record;
167
137
  }
168
138
  function resolveDriverPair(caller) {
139
+ var args = arguments[1];
140
+ var options = arguments[2];
169
141
  if (!caller) throw new Error("pair lifecycle tools require a session-bound tool server");
170
142
  if (sm.sessions.get(caller.localId) !== caller) {
171
143
  throw new Error("this session is no longer live; the pair tools are bound to an exact session");
@@ -176,24 +148,25 @@ function attachPairLifecycle(ctx) {
176
148
  if (!group) throw new Error("this session is not in a split group");
177
149
  if (!group.pair) throw new Error("this split group has no Driver/Split Worker roles");
178
150
  if (group.pair.driverId !== caller.localId) throw new Error("only the configured Driver can direct this pair");
179
- var worker = sm.sessions.get(group.pair.workerId);
180
- if (!worker) throw new Error("split partner session was not found");
151
+ var targetOptions = Object.assign({}, options || { mutation: true }, {
152
+ multiWorkerFeature: ctx.multiWorkerFeature,
153
+ });
154
+ var resolved = pairTarget.resolveGroupTarget(sm, group, caller, args, targetOptions);
155
+ var worker = resolved.partner;
181
156
  if ((caller.ownerId || null) !== (worker.ownerId || null)) throw new Error("split partner access denied");
182
- return { group: group, worker: worker };
157
+ return { group: resolved.group, worker: resolved.partner, role: resolved.role, workerId: resolved.workerId };
183
158
  }
184
-
185
159
  function replaceBlockedReason(worker) {
186
160
  if (worker.isProcessing || worker._queryStarting) return "the Split Worker is mid-turn";
187
161
  if (worker._pairDelegation) return "a delegated task is still open";
188
162
  return null;
189
163
  }
190
-
191
- function partnerStatus(caller) {
192
- var resolved = resolveDriverPair(caller);
164
+ function partnerStatus(caller, args) {
165
+ var resolved = resolveDriverPair(caller, args, { readOnly: true });
193
166
  var worker = resolved.worker;
194
167
  var blocked = turnControl.blockedReason(caller) || replaceBlockedReason(worker);
195
168
  var resumeBlocked = turnControl.blockedReason(caller) || (worker.isProcessing || worker._queryStarting ? "the Split Worker is mid-turn" : (!worker._pairInterruption ? "the Split Worker has no interrupted task" : null));
196
- var ledger = ledgerFor(caller).map(function (record) {
169
+ var ledger = ledgerFor(caller, worker).map(function (record) {
197
170
  return {
198
171
  generation: record.generation,
199
172
  vendor: record.vendor,
@@ -245,18 +218,16 @@ function attachPairLifecycle(ctx) {
245
218
  generations: ledger,
246
219
  };
247
220
  }
248
-
249
221
  function sourcePairRegistered(caller, worker) {
250
222
  try {
251
223
  if (sm.sessions.get(caller.localId) !== caller || sm.sessions.get(worker.localId) !== worker) return false;
252
224
  if ((caller.ownerId || null) !== (worker.ownerId || null)) return false;
253
225
  var group = store.groupForMember(caller.localId);
254
- return !!(group && group.pair && group.pair.driverId === caller.localId &&
255
- group.pair.workerId === worker.localId && group.members.indexOf(caller.localId) !== -1 &&
256
- group.members.indexOf(worker.localId) !== -1);
226
+ var normalized = splitRoles.normalizePair(group && group.pair, group && group.members);
227
+ return !!(normalized.ok && normalized.kind !== "adhoc" && normalized.driverId === caller.localId &&
228
+ normalized.workerIds.indexOf(worker.localId) !== -1);
257
229
  } catch (e) { return false; }
258
230
  }
259
-
260
231
  function restoreSourcePair(ws, caller, worker) {
261
232
  try {
262
233
  return store.create(ws, {
@@ -265,23 +236,25 @@ function attachPairLifecycle(ctx) {
265
236
  });
266
237
  } catch (e) { return { ok: false, error: e.message || String(e) }; }
267
238
  }
268
-
269
- function replacementTargetRegistered(caller, oldWorker) {
239
+ function replacementTargetRegistered(caller, oldWorker, sourceWorkerIds) {
270
240
  try {
271
241
  if (sm.sessions.get(caller.localId) !== caller || sm.sessions.get(oldWorker.localId) !== oldWorker) return null;
272
242
  var group = store.groupForMember(caller.localId);
273
- if (!group || !group.pair || group.pair.driverId !== caller.localId ||
274
- group.pair.workerId === oldWorker.localId) return null;
275
- var target = sm.sessions.get(group.pair.workerId);
243
+ var normalized = splitRoles.normalizePair(group && group.pair, group && group.members);
244
+ if (!normalized.ok || normalized.kind === "adhoc" || normalized.driverId !== caller.localId) return null;
245
+ var candidates = normalized.workerIds.filter(function (id) { return sourceWorkerIds.indexOf(id) === -1; });
246
+ if (candidates.length !== 1) return null;
247
+ var target = sm.sessions.get(candidates[0]);
276
248
  if (!target || sm.sessions.get(target.localId) !== target) return null;
277
249
  return (caller.ownerId || null) === (target.ownerId || null) ? target : null;
278
250
  } catch (e) { return null; }
279
251
  }
280
-
281
252
  function replacePartner(args, caller) {
282
- var resolved = resolveDriverPair(caller);
253
+ var resolved = resolveDriverPair(caller, args, { mutation: true });
283
254
  var group = resolved.group;
284
255
  var oldWorker = resolved.worker;
256
+ var versioned = resolved.role.kind === "versioned";
257
+ var sourceWorkerIds = resolved.role.workerIds.slice();
285
258
  ledgerFor(caller);
286
259
  turnControl.assertWorkerAction(caller);
287
260
  var replacementEntry = replacementState.begin(caller, args.transactionId, oldWorker, replacementFingerprint(args, oldWorker));
@@ -336,9 +309,11 @@ function attachPairLifecycle(ctx) {
336
309
  ctx.cancelWorkerPermissions(oldWorker, "The Driver replaced this Split Worker.");
337
310
  }
338
311
 
339
- replacementState.stage(replacementEntry.transaction, "dissolving_source_pair");
340
- var dissolved = store.dissolve(ws, { id: group.id });
341
- if (!dissolved.ok) throw new Error(dissolved.error || "could not dissolve the existing pair");
312
+ if (!versioned) {
313
+ replacementState.stage(replacementEntry.transaction, "dissolving_source_pair");
314
+ var dissolved = store.dissolve(ws, { id: group.id });
315
+ if (!dissolved.ok) throw new Error(dissolved.error || "could not dissolve the existing pair");
316
+ }
342
317
  } catch (precreateError) {
343
318
  var sourceIntact = sourcePairRegistered(caller, oldWorker);
344
319
  var precreateRestored = sourceIntact ? { ok: true } : restoreSourcePair(ws, caller, oldWorker);
@@ -351,13 +326,20 @@ function attachPairLifecycle(ctx) {
351
326
  var created;
352
327
  try {
353
328
  replacementState.stage(replacementEntry.transaction, "creating_target");
354
- created = ctx.createWorkerForDriver(caller, {
329
+ var createReplacement = versioned ? ctx.replaceWorkerForDriver : ctx.createWorkerForDriver;
330
+ created = createReplacement(caller, versioned ? oldWorker : {
355
331
  workerVendor: typeof args.workerVendor === "string" ? args.workerVendor : "",
356
332
  workerModel: typeof args.workerModel === "string" ? args.workerModel : "",
357
333
  workerEffort: typeof args.workerEffort === "string" ? args.workerEffort : "",
358
- });
334
+ }, versioned ? {
335
+ workerVendor: typeof args.workerVendor === "string" ? args.workerVendor : "",
336
+ workerModel: typeof args.workerModel === "string" ? args.workerModel : "",
337
+ workerEffort: typeof args.workerEffort === "string" ? args.workerEffort : "",
338
+ expectedGroup: group,
339
+ expectedWorkerIds: sourceWorkerIds,
340
+ } : undefined);
359
341
  } catch (e) {
360
- var target = replacementTargetRegistered(caller, oldWorker);
342
+ var target = replacementTargetRegistered(caller, oldWorker, sourceWorkerIds);
361
343
  if (target) {
362
344
  var targetClosed = closeGeneration(caller, oldWorker);
363
345
  var targetGeneration = recordGenerationStart(caller, target);
@@ -404,7 +386,7 @@ function attachPairLifecycle(ctx) {
404
386
 
405
387
  var message = typeof args.message === "string" ? args.message.trim() : "";
406
388
  if (!message) return Promise.resolve(result);
407
- return Promise.resolve(ctx.sendToPartner({ message: message, wait: args.wait, timeoutSeconds: args.timeoutSeconds }, caller))
389
+ return Promise.resolve(ctx.sendToPartner({ message: message, wait: args.wait, timeoutSeconds: args.timeoutSeconds, workerId: created.worker.localId }, caller))
408
390
  .then(function (delivered) {
409
391
  result.delivery = delivered;
410
392
  return result;
@@ -430,8 +412,8 @@ function attachPairLifecycle(ctx) {
430
412
  }
431
413
 
432
414
  function recordEvaluation(args, caller) {
433
- var resolved = resolveDriverPair(caller);
434
- var ledger = ledgerFor(caller);
415
+ var resolved = resolveDriverPair(caller, args, { mutation: true });
416
+ var ledger = ledgerFor(caller, resolved.worker);
435
417
  var target = Number.isInteger(args.generation)
436
418
  ? (function () {
437
419
  for (var i = 0; i < ledger.length; i++) {
@@ -453,14 +435,14 @@ function attachPairLifecycle(ctx) {
453
435
  };
454
436
  }
455
437
 
456
- function optionalStatus(boundSession) {
457
- try { return partnerStatus(boundSession); } catch (e) { return null; }
438
+ function optionalStatus(boundSession, args) {
439
+ try { return partnerStatus(boundSession, args); } catch (e) { return null; }
458
440
  }
459
441
 
460
442
  function toolHandlers(boundSession) {
461
443
  return {
462
- status: function () {
463
- try { return toolResult(partnerStatus(boundSession)); }
444
+ status: function (args) {
445
+ try { return toolResult(partnerStatus(boundSession, args || {})); }
464
446
  catch (e) { return toolError(e.message || String(e)); }
465
447
  },
466
448
  replace: function (args) {
@@ -17,7 +17,7 @@ function attachPairMessage(ctx) {
17
17
  if (!id) return toolResult({ status: "rejected", requestId: null, reason: "requestId is invalid" });
18
18
  try {
19
19
  ctx.turnControl.assertWorkerAction(caller);
20
- var resolved = ctx.resolvePair(caller);
20
+ var resolved = ctx.resolvePair(caller, args, { mutation: true });
21
21
  var worker = resolved.partner;
22
22
  var message = typeof args.message === "string" ? args.message.trim() : "";
23
23
  if (!message) return toolResult({ status: "rejected", requestId: id, partnerId: worker.localId, reason: "message is required" });
@@ -1,10 +1,13 @@
1
1
  function stopOwnedPair(session, roles, turnControl, taskControl, workerPermission, reason) {
2
2
  if (!roles) return false;
3
3
  turnControl.markHumanStop(session);
4
- taskControl.cancelAll(roles.worker);
5
- taskControl.markInterruption(roles.worker, "user", reason || "The human stopped this scheduled run.");
6
- workerPermission.cancelForSession(roles.worker, reason || "The human stopped this scheduled run.");
7
- var members = [roles.driver, roles.worker];
4
+ var workers = roles.workers || [roles.worker];
5
+ for (var wi = 0; wi < workers.length; wi++) {
6
+ taskControl.cancelAll(workers[wi]);
7
+ taskControl.markInterruption(workers[wi], "user", reason || "The human stopped this scheduled run.");
8
+ workerPermission.cancelForSession(workers[wi], reason || "The human stopped this scheduled run.");
9
+ }
10
+ var members = [roles.driver].concat(workers);
8
11
  for (var i = 0; i < members.length; i++) {
9
12
  members[i].taskStopRequested = true;
10
13
  if (members[i].queryInstance && typeof members[i].queryInstance.close === "function") {
@@ -1,4 +1,10 @@
1
1
  function attachPairResultCapture(ctx) {
2
+ var splitRoles = require("./session-split-group-roles");
3
+ function current(caller, partner, token) {
4
+ var group = ctx.store.groupForMember(partner.localId);
5
+ return !!(group && group.id === token.groupId && partner._pairDelegation === token &&
6
+ splitRoles.matchesLiveTarget(ctx.sm, group, caller, partner) && splitRoles.tokenMatchesWorker(partner, token));
7
+ }
2
8
  function begin(caller, partner, token, message, projectSlug) {
3
9
  if (!ctx.resultOutbox) return { ok: true };
4
10
  var driverOriginId = ctx.resultOutbox.stableOrigin(caller), workerOriginId = ctx.resultOutbox.stableOrigin(partner);
@@ -6,14 +12,15 @@ function attachPairResultCapture(ctx) {
6
12
  var started = ctx.resultOutbox.begin({ ownerId: caller.ownerId, projectSlug: projectSlug,
7
13
  driverOriginId: driverOriginId, workerOriginId: workerOriginId,
8
14
  taskId: token.taskId, generation: token.generation, driverSessionId: caller.localId,
9
- workerSessionId: partner.localId, historyStartIndex: token.startIndex, message: message,
15
+ workerSessionId: partner.localId, groupId: token.groupId,
16
+ historyStartIndex: token.startIndex, message: message,
10
17
  deliveryRoute: token.wait === true ? "blocking" : "callback" });
11
18
  if (started.ok) token.outboxKey = started.key;
12
19
  return started;
13
20
  }
14
21
  function finish(caller, partner, token, delivered) {
15
22
  var group = ctx.store.groupForMember(partner.localId);
16
- if (token._captureFinished || partner._pairDelegation !== token || !group || group.id !== token.groupId || group.members.indexOf(caller.localId) === -1 || group.members.indexOf(partner.localId) === -1 || (group.pair && (group.pair.driverId !== caller.localId || group.pair.workerId !== partner.localId)) || ctx.sm.sessions.get(caller.localId) !== caller || ctx.sm.sessions.get(partner.localId) !== partner || caller.ownerId !== partner.ownerId) return false;
23
+ if (token._captureFinished || !current(caller, partner, token)) return false;
17
24
  token._captureFinished = true;
18
25
  try {
19
26
  if (typeof ctx.onPartnerResult === "function") ctx.onPartnerResult(caller, partner, token);
@@ -37,7 +44,7 @@ function attachPairResultCapture(ctx) {
37
44
  }
38
45
  delete token._capturePending;
39
46
  var resumed = delivered ? true : ctx.resumeDriverWithResult(caller, partner, token);
40
- ctx.drain(caller, partner);
47
+ if (!partner._pairClosing) ctx.drain(caller, partner);
41
48
  return resumed;
42
49
  }
43
50
  function capture(caller, partner, token, outcome) {
@@ -50,12 +57,12 @@ function attachPairResultCapture(ctx) {
50
57
  token._captureInProgress = false;
51
58
  if (retryResult && retryResult.ok) {
52
59
  token._captureComplete = true;
53
- var record = ctx.resultOutbox.get(token.outboxKey);
54
- if (typeof ctx.onStateChange === "function") ctx.onStateChange(caller, record);
55
- if (record && record.deliveryRoute === "callback") deliver(caller, partner, token);
60
+ var capturedRecord = ctx.resultOutbox.get(token.outboxKey);
61
+ if (typeof ctx.onStateChange === "function") ctx.onStateChange(caller, capturedRecord);
62
+ if (capturedRecord && capturedRecord.deliveryRoute === "callback") deliver(caller, partner, token);
56
63
  else finish(caller, partner, token);
57
64
  }
58
- });
65
+ }, function () { return current(caller, partner, token); });
59
66
  } catch (error) {
60
67
  token._captureInProgress = false;
61
68
  token._capturePending = true;
@@ -64,9 +71,9 @@ function attachPairResultCapture(ctx) {
64
71
  }
65
72
  if (saved.ok) { token._captureInProgress = false; token._captureComplete = true; }
66
73
  if (saved.ok) {
67
- var current = ctx.resultOutbox.get(token.outboxKey);
68
- if (typeof ctx.onStateChange === "function") ctx.onStateChange(caller, current);
69
- var delivered = current && current.deliveryRoute === "callback" ? deliver(caller, partner, token) : finish(caller, partner, token);
74
+ var capturedRecord = ctx.resultOutbox.get(token.outboxKey);
75
+ if (typeof ctx.onStateChange === "function") ctx.onStateChange(caller, capturedRecord);
76
+ var delivered = capturedRecord && capturedRecord.deliveryRoute === "callback" ? deliver(caller, partner, token) : finish(caller, partner, token);
70
77
  return delivered === false && token._capturePending ? false : delivered;
71
78
  }
72
79
  token._capturePending = true;
@@ -76,11 +83,34 @@ function attachPairResultCapture(ctx) {
76
83
  }
77
84
  return false;
78
85
  }
86
+ function prepare(caller, partner, token, outcome) {
87
+ if (!ctx.resultOutbox || !token.outboxKey) return { ok: true, prepared: false };
88
+ var saved = ctx.resultOutbox.prepareCapture(token.outboxKey, outcome);
89
+ return saved.ok ? { ok: true, prepared: true, duplicate: !!saved.duplicate } : saved;
90
+ }
91
+ function rollbackPrepared(token, outcome) {
92
+ if (!ctx.resultOutbox || !token.outboxKey) return { ok: true };
93
+ return ctx.resultOutbox.rollbackPreparedCapture(token.outboxKey, outcome);
94
+ }
95
+ function finalizePrepared(token, outcome) {
96
+ if (!ctx.resultOutbox || !token.outboxKey) return { ok: true, record: null };
97
+ return ctx.resultOutbox.finalizePreparedCapture(token.outboxKey, outcome);
98
+ }
99
+ function acceptPrepared(caller, partner, token) {
100
+ if (ctx.resultOutbox && token.outboxKey) {
101
+ token._captureComplete = true;
102
+ var record = ctx.resultOutbox.get(token.outboxKey);
103
+ if (typeof ctx.onStateChange === "function") ctx.onStateChange(caller, record);
104
+ return record && record.deliveryRoute === "callback" ? deliver(caller, partner, token) : finish(caller, partner, token);
105
+ }
106
+ return finish(caller, partner, token);
107
+ }
79
108
  function deliver(caller, partner, token) {
80
109
  if (!ctx.delivery) return finish(caller, partner, token);
81
110
  return ctx.delivery.wake(caller, partner, token);
82
111
  }
83
- return { begin: begin, capture: capture, deliver: deliver, finish: finish };
112
+ return { begin: begin, capture: capture, prepare: prepare, finalizePrepared: finalizePrepared, rollbackPrepared: rollbackPrepared,
113
+ acceptPrepared: acceptPrepared, deliver: deliver, finish: finish };
84
114
  }
85
115
 
86
116
  module.exports = { attachPairResultCapture: attachPairResultCapture };
@@ -1,5 +1,6 @@
1
1
  var RETRY_DELAY_MS = 50;
2
2
  var DEFAULT_ACCEPTANCE_TIMEOUT_MS = 15000;
3
+ var splitRoles = require("./session-split-group-roles");
3
4
 
4
5
  function attachPairResultDelivery(ctx) {
5
6
  function recordMatches(caller, partner, token, record) {
@@ -9,9 +10,9 @@ function attachPairResultDelivery(ctx) {
9
10
  }
10
11
  function valid(caller, partner, token) {
11
12
  var group = ctx.store.groupForMember(partner.localId), record = ctx.outbox.get(token.outboxKey);
12
- return !!(group && group.id === token.groupId && group.members.indexOf(caller.localId) !== -1 &&
13
- group.members.indexOf(partner.localId) !== -1 && group.pair && group.pair.driverId === caller.localId &&
14
- group.pair.workerId === partner.localId && ctx.sm.sessions.get(caller.localId) === caller &&
13
+ return !!(group && group.id === token.groupId && splitRoles.matchesLiveTarget(ctx.sm, group, caller, partner) &&
14
+ splitRoles.tokenMatchesWorker(partner, token) &&
15
+ ctx.sm.sessions.get(caller.localId) === caller &&
15
16
  ctx.sm.sessions.get(partner.localId) === partner && caller.ownerId === partner.ownerId &&
16
17
  !caller.destroying && !partner.destroying && !ctx.blockedReason(caller) && partner._pairDelegation === token &&
17
18
  recordMatches(caller, partner, token, record));