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.
- package/lib/driver-continuation-pair.js +5 -1
- package/lib/multi-worker-feature.js +11 -0
- package/lib/project-pair-autonomous-stop.js +14 -8
- package/lib/project-pair-close-transaction.js +59 -0
- package/lib/project-pair-global-stop.js +18 -0
- package/lib/project-pair-lifecycle-status.js +34 -0
- package/lib/project-pair-lifecycle.js +58 -76
- package/lib/project-pair-message.js +1 -1
- package/lib/project-pair-owned-stop.js +7 -4
- package/lib/project-pair-result-capture.js +41 -11
- package/lib/project-pair-result-delivery.js +4 -3
- package/lib/project-pair-result-outbox.js +73 -4
- package/lib/project-pair-result-recovery.js +6 -2
- package/lib/project-pair-structural-close.js +20 -0
- package/lib/project-pair-task-control.js +39 -18
- package/lib/project-pair-worker-close.js +68 -0
- package/lib/project-session-pair-prompt.js +26 -0
- package/lib/project-session-pair.js +55 -78
- package/lib/project-worker-creation-proposal.js +176 -0
- package/lib/project-worker-permission.js +7 -8
- package/lib/project-worker-proposal-runner.js +45 -0
- package/lib/project-worker-proposal.js +79 -115
- package/lib/project.js +15 -0
- package/lib/public/css/pane.css +48 -2
- package/lib/public/modules/app-connection.js +49 -76
- package/lib/public/modules/app-messages.js +4 -0
- package/lib/public/modules/project-activation.js +10 -7
- package/lib/public/modules/sidebar-sessions.js +31 -11
- package/lib/public/modules/split-group-helpers.js +74 -2
- package/lib/public/modules/split-pair-ui.js +28 -6
- package/lib/public/modules/split-pane-reconciler.js +28 -0
- package/lib/public/modules/split-pane-renderer.js +107 -0
- package/lib/public/modules/split-view.js +58 -188
- package/lib/public/modules/websocket-watchdog.js +126 -0
- package/lib/public/modules/worker-pane-lock.js +13 -3
- package/lib/server.js +1 -0
- package/lib/session-pair-factory.js +66 -0
- package/lib/session-pair-mcp-server.js +8 -3
- package/lib/session-pair-prompts.js +34 -24
- package/lib/session-pair-target.js +42 -0
- package/lib/session-pair-turn-control.js +11 -6
- package/lib/session-split-group-anchors.js +89 -7
- package/lib/session-split-group-persistence.js +136 -0
- package/lib/session-split-group-roles.js +164 -0
- package/lib/session-split-group-v2-store.js +146 -0
- package/lib/session-split-groups.js +109 -99
- package/lib/ws-schema.js +2 -0
- 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
|
-
|
|
27
|
-
|
|
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
|
|
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
|
|
207
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
{ name: "
|
|
274
|
-
{ name: "
|
|
275
|
-
{ name: "
|
|
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,
|
|
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 };
|