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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/lib/driver-continuation-pair.js +5 -1
  2. package/lib/multi-worker-feature.js +11 -0
  3. package/lib/project-connection.js +2 -0
  4. package/lib/project-pair-autonomous-stop.js +14 -8
  5. package/lib/project-pair-close-transaction.js +59 -0
  6. package/lib/project-pair-global-stop.js +18 -0
  7. package/lib/project-pair-lifecycle-status.js +34 -0
  8. package/lib/project-pair-lifecycle.js +58 -76
  9. package/lib/project-pair-message.js +1 -1
  10. package/lib/project-pair-owned-stop.js +7 -4
  11. package/lib/project-pair-result-capture.js +116 -0
  12. package/lib/project-pair-result-delivery.js +140 -0
  13. package/lib/project-pair-result-outbox.js +400 -0
  14. package/lib/project-pair-result-pipeline.js +16 -0
  15. package/lib/project-pair-result-recovery.js +229 -0
  16. package/lib/project-pair-structural-close.js +20 -0
  17. package/lib/project-pair-task-control.js +45 -18
  18. package/lib/project-pair-worker-close.js +68 -0
  19. package/lib/project-session-pair-prompt.js +26 -0
  20. package/lib/project-session-pair.js +87 -106
  21. package/lib/project-worker-creation-proposal.js +176 -0
  22. package/lib/project-worker-permission.js +7 -8
  23. package/lib/project-worker-proposal-runner.js +45 -0
  24. package/lib/project-worker-proposal.js +79 -115
  25. package/lib/project.js +38 -1
  26. package/lib/public/app.js +1 -0
  27. package/lib/public/css/messages.css +12 -0
  28. package/lib/public/css/pane.css +48 -2
  29. package/lib/public/modules/app-connection.js +49 -76
  30. package/lib/public/modules/app-messages.js +11 -0
  31. package/lib/public/modules/pair-result-status.js +87 -0
  32. package/lib/public/modules/project-activation.js +10 -7
  33. package/lib/public/modules/sidebar-sessions.js +31 -11
  34. package/lib/public/modules/split-group-helpers.js +74 -2
  35. package/lib/public/modules/split-pair-ui.js +28 -6
  36. package/lib/public/modules/split-pane-reconciler.js +28 -0
  37. package/lib/public/modules/split-pane-renderer.js +107 -0
  38. package/lib/public/modules/split-view.js +58 -188
  39. package/lib/public/modules/websocket-watchdog.js +126 -0
  40. package/lib/public/modules/worker-pane-lock.js +13 -3
  41. package/lib/server.js +1 -0
  42. package/lib/session-pair-factory.js +66 -0
  43. package/lib/session-pair-mcp-server.js +10 -5
  44. package/lib/session-pair-prompts.js +41 -25
  45. package/lib/session-pair-target.js +42 -0
  46. package/lib/session-pair-turn-control.js +11 -6
  47. package/lib/session-split-group-anchors.js +89 -7
  48. package/lib/session-split-group-persistence.js +136 -0
  49. package/lib/session-split-group-roles.js +164 -0
  50. package/lib/session-split-group-v2-store.js +146 -0
  51. package/lib/session-split-groups.js +109 -99
  52. package/lib/sessions.js +33 -2
  53. package/lib/ws-schema.js +2 -0
  54. package/package.json +1 -1
@@ -0,0 +1,229 @@
1
+ var crypto = require("crypto");
2
+ var splitRoles = require("./session-split-group-roles");
3
+
4
+ function attachPairResultRecovery(ctx) {
5
+ var scheduled = Object.create(null);
6
+ var runtimeStatus = Object.create(null);
7
+
8
+ function opaqueId(key) {
9
+ return crypto.createHash("sha256").update(String(key)).digest("hex").slice(0, 24);
10
+ }
11
+ function ownerId(session) { return session && session.ownerId || null; }
12
+ function findOrigin(originId) {
13
+ var found = null, duplicate = false;
14
+ ctx.sm.sessions.forEach(function (session) {
15
+ if (!session || session.sessionOriginId !== originId) return;
16
+ if (found) duplicate = true;
17
+ else found = session;
18
+ });
19
+ return duplicate ? null : found;
20
+ }
21
+ function resolve(record) {
22
+ if (!record || record.projectSlug !== ctx.projectSlug) return { ok: false, code: "project", reason: "This completed result belongs to a different project." };
23
+ var driver = findOrigin(record.driverOriginId), worker = findOrigin(record.workerOriginId);
24
+ if (!driver || !worker) return { ok: false, code: "session", reason: "The original Driver or Split Worker session is unavailable." };
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." };
26
+ var group = ctx.store.groupForMember(driver.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) {
30
+ return { ok: false, code: "pair", reason: "The original Driver and Split Worker pair is no longer active." };
31
+ }
32
+ var provenance = worker.sessionProvenance;
33
+ if (!provenance || provenance.kind !== "worker" || provenance.parentSessionOriginId !== record.driverOriginId || provenance.generation !== record.generation) {
34
+ return { ok: false, code: "generation", reason: "The Split Worker generation no longer matches this completed result." };
35
+ }
36
+ if (worker._pairDelegation && (worker._pairDelegation.outboxKey !== record.key || worker._pairDelegation.taskId !== record.taskId)) {
37
+ return { ok: false, code: "active_task", reason: "A newer Split Worker task is active, so this result cannot be restored automatically." };
38
+ }
39
+ return { ok: true, driver: driver, worker: worker, group: group };
40
+ }
41
+ function tokenFor(record, resolved) {
42
+ var token = resolved.worker._pairDelegation;
43
+ if (token && token.outboxKey === record.key) {
44
+ if (Number(record.manualRetryEpoch || 0) > Number(token._manualRetryEpochApplied || 0)) {
45
+ if (token._deliveryTimer && token._deliveryTimer.timer) clearTimeout(token._deliveryTimer.timer);
46
+ delete token._deliveryTimer; delete token._deliveryUncertain; delete token._deliveryAccepted;
47
+ delete token._deliveryStartupAttemptId;
48
+ token._manualRetryEpochApplied = Number(record.manualRetryEpoch || 0);
49
+ }
50
+ return token;
51
+ }
52
+ token = {
53
+ outboxKey: record.key, taskId: record.taskId, generation: record.generation,
54
+ workerSessionId: resolved.worker.localId, workerId: resolved.worker.localId,
55
+ from: resolved.driver.localId, groupId: resolved.group.id,
56
+ startIndex: Number.isInteger(record.historyStartIndex) ? record.historyStartIndex : resolved.worker.history.length,
57
+ message: record.message || "", wait: record.deliveryRoute === "blocking",
58
+ detached: record.deliveryRoute === "callback", detachRequested: record.deliveryRoute === "callback",
59
+ response: record.outcome && record.outcome.response || "",
60
+ failure: record.outcome && record.outcome.error || null,
61
+ interrupted: !!(record.outcome && record.outcome.status === "interrupted"),
62
+ _captureComplete: true,
63
+ _manualRetryEpochApplied: Number(record.manualRetryEpoch || 0),
64
+ };
65
+ resolved.worker._pairDelegation = token;
66
+ resolved.worker._delegatedBy = resolved.driver.localId;
67
+ return token;
68
+ }
69
+ function block(record, reason, code) {
70
+ if (record.deliveryState === "accepted" && record.finalized) return;
71
+ var saved = ctx.outbox.markBlocked(record.key, reason, code);
72
+ if (saved.ok) changed();
73
+ }
74
+ function process(key, allowDelivery) {
75
+ delete scheduled[key];
76
+ var record = ctx.outbox.get(key);
77
+ if (!record || !record.outcome) return { ok: false, deferred: true };
78
+ if (record.finalized) return { ok: true, finalized: true };
79
+ if (record.blockedCode === "human_stop") return { ok: false, blocked: true };
80
+ var resolved = resolve(record);
81
+ if (!resolved.ok) {
82
+ block(record, resolved.reason, resolved.code);
83
+ return { ok: false, blocked: true };
84
+ }
85
+ var stopReason = typeof ctx.blockedReason === "function" ? ctx.blockedReason(resolved.driver) : null;
86
+ if (stopReason) {
87
+ block(record, stopReason, "human_stop");
88
+ return { ok: false, blocked: true };
89
+ }
90
+ if (record.finalizationState === "failed") return { ok: false, blocked: true };
91
+ var token = tokenFor(record, resolved);
92
+ if (record.deliveryRoute === "blocking" || record.deliveryState === "accepted") {
93
+ var finished = ctx.resultCapture.finish(resolved.driver, resolved.worker, token, record.deliveryState === "accepted");
94
+ changed(resolved.driver);
95
+ return { ok: finished !== false, finalized: finished !== false };
96
+ }
97
+ if (record.deliveryState === "uncertain") return { ok: false, uncertain: true };
98
+ if (!allowDelivery) return { ok: false, deferred: true };
99
+ var result = ctx.wake(resolved.driver, resolved.worker, token);
100
+ changed(resolved.driver);
101
+ return result;
102
+ }
103
+ function schedule(key) {
104
+ if (scheduled[key]) return;
105
+ scheduled[key] = true;
106
+ setImmediate(function () { process(key, true); });
107
+ }
108
+ function healthyLiveDispatch(record) {
109
+ if (!record || record.outcome || record.state === "blocked") return false;
110
+ var resolved = resolve(record);
111
+ if (!resolved.ok) return false;
112
+ var token = resolved.worker._pairDelegation;
113
+ return !!(token && token.outboxKey === record.key && token.taskId === record.taskId && token.generation === record.generation &&
114
+ token.from === resolved.driver.localId && token.groupId === resolved.group.id && !token._capturePending && !token._captureHookError &&
115
+ (resolved.worker.isProcessing || resolved.worker._queryStarting));
116
+ }
117
+ function recordsFor(session) {
118
+ if (!session) return [];
119
+ var originId = session.sessionOriginId, currentOwner = ownerId(session);
120
+ return ctx.outbox.list().map(function (record) {
121
+ return runtimeStatus[record.key] ? Object.assign({}, record, runtimeStatus[record.key]) : record;
122
+ }).filter(function (record) {
123
+ if (record.projectSlug !== ctx.projectSlug || record.driverOriginId !== originId || (record.ownerId || null) !== currentOwner) return false;
124
+ if (record.state === "close_prepared") return false;
125
+ if (healthyLiveDispatch(record)) return false;
126
+ if (record.finalized) return false;
127
+ if (record.deliveryState === "accepted") return record.state === "blocked" || record.finalizationState === "failed";
128
+ return true;
129
+ });
130
+ }
131
+ function publicItem(record) {
132
+ var completed = !!record.outcome;
133
+ var finalizationFailed = record.finalizationState === "failed";
134
+ var state = record.state === "blocked" || finalizationFailed ? "blocked" : record.deliveryState;
135
+ if (state === "attempting") state = "uncertain";
136
+ if (!completed && state !== "blocked") state = "uncertain";
137
+ var exhausted = state === "pending" && Number(record.deliveryAttemptCount || 0) >= ctx.outbox.maxDeliveryAttempts;
138
+ return {
139
+ id: opaqueId(record.key), state: exhausted ? "pending" : state,
140
+ message: record.blockedReason || record.finalizationError || (!completed ? "The Split Worker task was retained, but no durable completed result is available yet." :
141
+ (state === "uncertain" ? "Delivery could not be confirmed. The Driver may already have received this result." :
142
+ (exhausted ? "Automatic delivery could not complete. The finished result is retained." : "The finished Split Worker result is waiting to be delivered."))),
143
+ response: completed ? record.outcome.response || "" : "", error: completed ? record.outcome.error || null : null,
144
+ hasCompletedResult: completed,
145
+ canRetry: completed && !finalizationFailed && record.blockedCode !== "human_stop" && Number(record.manualRetryCount || 0) < 1 && (state === "uncertain" || exhausted),
146
+ confirmDuplicate: completed && state === "uncertain",
147
+ };
148
+ }
149
+ function canRead(ws, session) {
150
+ if (!ws || !session || ws._clayActiveSession !== session.localId) return false;
151
+ if (ownerId(session)) return !!(ws._clayUser && String(ws._clayUser.id) === String(ownerId(session)));
152
+ return !ws._clayUser || !ctx.isMultiUser();
153
+ }
154
+ function sendState(session, ws, sendFn) {
155
+ if (!canRead(ws, session)) return false;
156
+ var items = recordsFor(session).map(publicItem);
157
+ (sendFn || ctx.sendTo.bind(null, ws))({ type: "pair_result_status", sessionId: session.localId, items: items });
158
+ return true;
159
+ }
160
+ function changed(driver) {
161
+ var targets = typeof ctx.getClients === "function" ? ctx.getClients() : [];
162
+ for (var ws of targets) {
163
+ var session = driver || ctx.sm.sessions.get(ws._clayActiveSession);
164
+ if (session) sendState(session, ws);
165
+ }
166
+ }
167
+ function stateChanged(driver, record) {
168
+ if (record && record.key && record.persistenceError) runtimeStatus[record.key] = {
169
+ finalizationState: record.finalizationState, finalizationError: record.finalizationError,
170
+ };
171
+ if (record && record.key && record.finalized) delete runtimeStatus[record.key];
172
+ changed(driver);
173
+ if (record && !record.finalized && record.finalizationState !== "failed" && (record.deliveryState === "accepted" || record.deliveryRoute === "blocking")) schedule(record.key);
174
+ }
175
+ function available(session) {
176
+ if (!session) return;
177
+ var origin = session.sessionOriginId;
178
+ var records = ctx.outbox.list();
179
+ for (var i = 0; i < records.length; i++) {
180
+ if (records[i].driverOriginId === origin && records[i].outcome && records[i].deliveryState === "pending" && records[i].state !== "blocked") schedule(records[i].key);
181
+ }
182
+ }
183
+ function reconcile() {
184
+ var records = ctx.outbox.list();
185
+ for (var i = 0; i < records.length; i++) {
186
+ var record = records[i];
187
+ if (!record.outcome) continue;
188
+ if (record.deliveryState === "attempting") {
189
+ var uncertain = ctx.outbox.markRestartUncertain(record.key);
190
+ if (uncertain.ok && uncertain.record) record = uncertain.record;
191
+ }
192
+ process(record.key, false);
193
+ record = ctx.outbox.get(record.key);
194
+ if (record && record.deliveryState === "pending" && record.state !== "blocked" && record.finalizationState !== "failed") schedule(record.key);
195
+ }
196
+ }
197
+ function beginHumanTurn(session) {
198
+ var records = recordsFor(session);
199
+ for (var i = 0; i < records.length; i++) {
200
+ if (records[i].blockedCode !== "human_stop") continue;
201
+ var saved = ctx.outbox.authorizeHumanTurn(records[i].key);
202
+ if (saved.ok && saved.record.outcome && (saved.record.deliveryState === "pending" || saved.record.deliveryState === "accepted")) schedule(records[i].key);
203
+ }
204
+ changed(session);
205
+ }
206
+ function handleMessage(ws, msg) {
207
+ if (!msg || (msg.type !== "pair_result_state_request" && msg.type !== "pair_result_retry")) return false;
208
+ var session = ctx.sm.sessions.get(ws._clayActiveSession);
209
+ if (!canRead(ws, session)) return true;
210
+ if (msg.sessionId != null && msg.sessionId !== session.localId) return true;
211
+ if (msg.projectSlug && msg.projectSlug !== ctx.projectSlug) return true;
212
+ if (msg.type === "pair_result_state_request") { sendState(session, ws); return true; }
213
+ var records = recordsFor(session), record = null;
214
+ for (var i = 0; i < records.length; i++) if (opaqueId(records[i].key) === msg.id) record = records[i];
215
+ if (!record) { sendState(session, ws); return true; }
216
+ if (record.deliveryState === "uncertain" && msg.confirmDuplicate !== true) { sendState(session, ws); return true; }
217
+ var granted = ctx.outbox.grantManualRetry(record.key);
218
+ if (granted.ok) schedule(record.key);
219
+ changed(session);
220
+ return true;
221
+ }
222
+ if (ctx.sm && typeof ctx.sm.addOnSessionViewed === "function") {
223
+ ctx.sm.addOnSessionViewed(function (session, ws, sendFn) { available(session); sendState(session, ws, sendFn); });
224
+ }
225
+ return { available: available, beginHumanTurn: beginHumanTurn, changed: changed, stateChanged: stateChanged, handleMessage: handleMessage,
226
+ reconcile: reconcile, sendState: sendState, process: process };
227
+ }
228
+
229
+ module.exports = { attachPairResultRecovery: attachPairResultRecovery };
@@ -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,13 +230,29 @@ 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 };
243
+ if (ctx.sm.sendAndRecord) {
244
+ try { ctx.sm.sendAndRecord(worker, { type: "pair_task_completed", _internal: true,
245
+ ownerId: caller.ownerId || null, driverOriginId: caller.sessionOriginId || null,
246
+ workerOriginId: worker.sessionOriginId || null, taskId: token.taskId, generation: token.generation,
247
+ outcome: envelope }); } catch (evidenceError) { token._completionEvidenceError = evidenceError.message || String(evidenceError); }
248
+ }
231
249
  if (ctx.sm.sendToSession) ctx.sm.sendToSession(caller, { type: "partner_task_completed", outcome: envelope });
232
250
  return envelope;
233
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
+ }
234
256
  function status(worker) {
235
257
  var active = current(worker);
236
258
  var registry = registryFor(worker);
@@ -255,23 +277,28 @@ function attachPairTaskControl(ctx) {
255
277
  next.status = "starting";
256
278
  next.startedAt = Date.now();
257
279
  queue.splice(queue.indexOf(next), 1);
258
- 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) {
259
281
  next.status = delivery && delivery.isError ? "failed" : "running";
260
282
  if (delivery && delivery.isError) next.error = delivery.content && delivery.content[0] && delivery.content[0].text || "Follow-up rejected";
261
283
  }, function (err) { next.status = "failed"; next.error = err.message || String(err); });
262
284
  return true;
263
285
  }
264
286
  function driverToolDefs(handlers) {
265
- 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 },
266
- { name: "inspect_partner_followups", description: "Inspect current, queued, interrupted, and last completed Split Worker task state.", inputSchema: buildShape({}), handler: handlers.inspect },
267
- { name: "cancel_partner_followup", description: "Cancel one exact queued follow-up without interrupting active work.", inputSchema: buildShape({ taskId: { type: "string" } }, ["taskId"]), handler: handlers.cancel },
268
- { 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 },
269
- { 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 }];
270
293
  }
271
294
  function workerToolDefs(worker) {
272
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); } }];
273
296
  }
274
- 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 };
275
302
  }
276
303
 
277
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 };