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.
- package/lib/driver-continuation-pair.js +5 -1
- package/lib/multi-worker-feature.js +11 -0
- package/lib/project-connection.js +2 -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 +116 -0
- package/lib/project-pair-result-delivery.js +140 -0
- package/lib/project-pair-result-outbox.js +400 -0
- package/lib/project-pair-result-pipeline.js +16 -0
- package/lib/project-pair-result-recovery.js +229 -0
- package/lib/project-pair-structural-close.js +20 -0
- package/lib/project-pair-task-control.js +45 -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 +87 -106
- 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 +38 -1
- package/lib/public/app.js +1 -0
- package/lib/public/css/messages.css +12 -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 +11 -0
- package/lib/public/modules/pair-result-status.js +87 -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 +10 -5
- package/lib/session-pair-prompts.js +41 -25
- 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/sessions.js +33 -2
- package/lib/ws-schema.js +2 -0
- 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
|
|
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,13 +230,29 @@ 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 };
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
{ name: "
|
|
268
|
-
{ name: "
|
|
269
|
-
{ 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 }];
|
|
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,
|
|
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 };
|