clay-server 4.2.0 → 4.3.0-beta.10
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-access.js +54 -0
- package/lib/driver-continuation-lease.js +52 -0
- package/lib/driver-continuation-pair-transfer.js +86 -0
- package/lib/driver-continuation-pair.js +86 -0
- package/lib/driver-continuation-record.js +40 -0
- package/lib/driver-continuation-startup.js +68 -0
- package/lib/driver-continuation-transaction.js +61 -0
- package/lib/driver-continuation-trigger.js +110 -0
- package/lib/multi-worker-feature.js +11 -0
- package/lib/project-connection.js +3 -1
- package/lib/project-driver-continuation.js +499 -0
- package/lib/project-message-delivery.js +20 -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 +117 -135
- package/lib/project-pair-message.js +1 -1
- package/lib/project-pair-owned-stop.js +7 -4
- package/lib/project-pair-replacement-state.js +5 -1
- 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-handoff.js +48 -55
- package/lib/project-session-pair-prompt.js +26 -0
- package/lib/project-session-pair.js +87 -106
- package/lib/project-sessions.js +4 -0
- package/lib/project-user-message.js +21 -0
- 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 +87 -3
- package/lib/public/app.js +1 -0
- package/lib/public/css/driver-continuation.css +56 -0
- package/lib/public/css/messages.css +12 -0
- package/lib/public/css/mobile-nav.css +29 -14
- package/lib/public/css/overlays.css +54 -25
- package/lib/public/css/pane.css +66 -12
- package/lib/public/css/sidebar.css +62 -7
- package/lib/public/index.html +3 -3
- package/lib/public/modules/app-connection.js +165 -74
- package/lib/public/modules/app-messages.js +31 -3
- package/lib/public/modules/driver-continuation-state.js +76 -0
- package/lib/public/modules/driver-continuation.js +260 -0
- package/lib/public/modules/message-delivery-ui.js +63 -0
- package/lib/public/modules/message-delivery.js +163 -6
- package/lib/public/modules/pair-result-status.js +87 -0
- package/lib/public/modules/permission-control.js +10 -5
- package/lib/public/modules/project-activation.js +10 -7
- package/lib/public/modules/session-creation-cta.js +46 -0
- package/lib/public/modules/sidebar-mobile.js +42 -30
- package/lib/public/modules/sidebar-sessions.js +77 -52
- 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-lifecycle.js +178 -0
- package/lib/public/modules/websocket-watchdog.js +126 -0
- package/lib/public/modules/worker-pane-lock.js +13 -3
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +77 -0
- package/lib/server.js +1 -0
- package/lib/session-handoff-discovery.js +443 -0
- package/lib/session-handoff-mcp-server.js +32 -4
- package/lib/session-pair-factory.js +66 -0
- package/lib/session-pair-mcp-server.js +10 -5
- package/lib/session-pair-prompts.js +46 -25
- package/lib/session-pair-target.js +42 -0
- package/lib/session-pair-turn-control.js +30 -9
- package/lib/session-split-group-anchors.js +126 -0
- 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 +293 -133
- package/lib/sessions.js +89 -22
- package/lib/ws-schema.js +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
var pairUsage = require("./project-pair-usage");
|
|
2
|
+
|
|
3
|
+
var MAX_TASK_PREVIEW_CHARS = 200;
|
|
4
|
+
|
|
5
|
+
function clampText(value, max) {
|
|
6
|
+
var text = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
|
|
7
|
+
if (text.length > max) text = text.slice(0, max - 1) + "…";
|
|
8
|
+
return text;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function continuityStatus(session) {
|
|
12
|
+
var history = session.history || [];
|
|
13
|
+
var userTurns = 0;
|
|
14
|
+
var errors = 0;
|
|
15
|
+
for (var i = 0; i < history.length; i++) {
|
|
16
|
+
if (!history[i]) continue;
|
|
17
|
+
if (history[i].type === "user_message") userTurns++;
|
|
18
|
+
else if (history[i].type === "error") errors++;
|
|
19
|
+
}
|
|
20
|
+
var lastActivity = typeof session.lastActivity === "number" ? session.lastActivity : null;
|
|
21
|
+
return { historyEntries: history.length, userTurns: userTurns, errorEntries: errors,
|
|
22
|
+
idleSeconds: lastActivity ? Math.max(0, Math.round((Date.now() - lastActivity) / 1000)) : null };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function activityStatus(session) {
|
|
26
|
+
var token = session._pairDelegation || null;
|
|
27
|
+
return { isProcessing: !!(session.isProcessing || session._queryStarting), delegated: !!token,
|
|
28
|
+
currentTask: token ? clampText(token.message, MAX_TASK_PREVIEW_CHARS) : "",
|
|
29
|
+
currentTaskId: token && token.taskId || null, interruption: session._pairInterruption || null,
|
|
30
|
+
lastTurnInterrupted: !!session._lastTurnInterrupted };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { activityStatus: activityStatus, clampText: clampText, continuityStatus: continuityStatus,
|
|
34
|
+
contextStatus: pairUsage.contextStatus, firstNumber: pairUsage.firstNumber };
|
|
@@ -1,27 +1,16 @@
|
|
|
1
|
-
// Split Worker lifecycle internals: bounded status, transactional replacement,
|
|
2
|
-
// and the per-generation evaluation ledger. The Driver-facing tool posts a
|
|
3
|
-
// user-controlled runtime card before accepted replacement reaches this module.
|
|
4
|
-
//
|
|
5
|
-
// Everything is bound to the exact live pair. The Driver is re-resolved from
|
|
6
|
-
// the split store on every call, its structural capability is re-checked every time, and
|
|
7
|
-
// ownership must match on both sessions. No active-tab or global-session
|
|
8
|
-
// fallback exists in this module.
|
|
9
|
-
//
|
|
10
|
-
// What replacement deliberately does NOT do: delete history. Dissolving a pair
|
|
11
|
-
// leaves both sessions in the project exactly as close_partner already does, so
|
|
12
|
-
// the previous Worker's conversation stays browsable and recoverable under the
|
|
13
|
-
// existing session semantics. There is no archive concept in the repo to hook
|
|
14
|
-
// into, and inventing a destructive one would lose work.
|
|
15
|
-
|
|
16
1
|
var eligibility = require("./session-driver-eligibility");
|
|
17
2
|
var replacementState = require("./project-pair-replacement-state");
|
|
18
|
-
var
|
|
19
|
-
var contextStatus =
|
|
20
|
-
var firstNumber =
|
|
3
|
+
var lifecycleStatus = require("./project-pair-lifecycle-status");
|
|
4
|
+
var contextStatus = lifecycleStatus.contextStatus;
|
|
5
|
+
var firstNumber = lifecycleStatus.firstNumber;
|
|
6
|
+
var continuityStatus = lifecycleStatus.continuityStatus;
|
|
7
|
+
var activityStatus = lifecycleStatus.activityStatus;
|
|
8
|
+
var clampText = lifecycleStatus.clampText;
|
|
9
|
+
var pairTarget = require("./session-pair-target");
|
|
10
|
+
var splitRoles = require("./session-split-group-roles");
|
|
21
11
|
|
|
22
12
|
var MAX_GENERATIONS = 5;
|
|
23
13
|
var MAX_NOTE_CHARS = 400;
|
|
24
|
-
var MAX_TASK_PREVIEW_CHARS = 200;
|
|
25
14
|
|
|
26
15
|
var EVALUATION_OUTCOMES = ["succeeded", "partial", "failed", "abandoned"];
|
|
27
16
|
|
|
@@ -36,60 +25,23 @@ function toolError(message) {
|
|
|
36
25
|
});
|
|
37
26
|
}
|
|
38
27
|
|
|
39
|
-
function clampText(value, max) {
|
|
40
|
-
var text = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
|
|
41
|
-
if (text.length > max) text = text.slice(0, max - 1) + "…";
|
|
42
|
-
return text;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
28
|
function replacementFingerprint(args, worker) {
|
|
46
29
|
return JSON.stringify({ sourceWorkerId: worker.localId, interrupt: args.interrupt === true,
|
|
47
30
|
vendor: args.workerVendor || "", model: args.workerModel || "", effort: args.workerEffort || "",
|
|
48
31
|
message: args.message || "", evaluation: args.evaluation || null });
|
|
49
32
|
}
|
|
50
33
|
|
|
51
|
-
function continuityStatus(session) {
|
|
52
|
-
var history = session.history || [];
|
|
53
|
-
var userTurns = 0;
|
|
54
|
-
var errors = 0;
|
|
55
|
-
for (var i = 0; i < history.length; i++) {
|
|
56
|
-
if (!history[i]) continue;
|
|
57
|
-
if (history[i].type === "user_message") userTurns++;
|
|
58
|
-
else if (history[i].type === "error") errors++;
|
|
59
|
-
}
|
|
60
|
-
var lastActivity = typeof session.lastActivity === "number" ? session.lastActivity : null;
|
|
61
|
-
return {
|
|
62
|
-
historyEntries: history.length,
|
|
63
|
-
userTurns: userTurns,
|
|
64
|
-
errorEntries: errors,
|
|
65
|
-
idleSeconds: lastActivity ? Math.max(0, Math.round((Date.now() - lastActivity) / 1000)) : null,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// The current task, as a bounded preview of the delegated instruction only.
|
|
70
|
-
// Never the transcript: a Driver deciding reuse needs to know what the Worker
|
|
71
|
-
// is on, not to re-read its conversation.
|
|
72
|
-
function activityStatus(session) {
|
|
73
|
-
var token = session._pairDelegation || null;
|
|
74
|
-
return {
|
|
75
|
-
isProcessing: !!(session.isProcessing || session._queryStarting),
|
|
76
|
-
delegated: !!token,
|
|
77
|
-
currentTask: token ? clampText(token.message, MAX_TASK_PREVIEW_CHARS) : "",
|
|
78
|
-
currentTaskId: token && token.taskId || null,
|
|
79
|
-
interruption: session._pairInterruption || null,
|
|
80
|
-
lastTurnInterrupted: !!session._lastTurnInterrupted,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
34
|
function attachPairLifecycle(ctx) {
|
|
85
35
|
var sm = ctx.sm;
|
|
86
36
|
var store = ctx.splitStore;
|
|
87
37
|
var turnControl = ctx.turnControl;
|
|
88
38
|
|
|
89
|
-
function ledgerFor(driver) {
|
|
39
|
+
function ledgerFor(driver, selectedWorker) {
|
|
90
40
|
if (!Array.isArray(driver._workerGenerations)) driver._workerGenerations = [];
|
|
91
41
|
var group = store.groupForMember(driver.localId);
|
|
92
|
-
var
|
|
42
|
+
var normalized = splitRoles.normalizePair(group && group.pair, group && group.members);
|
|
43
|
+
var fallbackId = normalized.ok && normalized.kind !== "adhoc" && normalized.workerIds.length === 1 ? normalized.workerIds[0] : null;
|
|
44
|
+
var worker = selectedWorker || fallbackId != null && sm.sessions.get(fallbackId);
|
|
93
45
|
var generation = worker && (worker._pairGeneration || worker.sessionProvenance && worker.sessionProvenance.generation);
|
|
94
46
|
if (worker && Number.isInteger(generation) && generation > 0 && !findGeneration(driver, worker)) {
|
|
95
47
|
driver._workerGenerations.push({
|
|
@@ -109,7 +61,7 @@ function attachPairLifecycle(ctx) {
|
|
|
109
61
|
return driver._workerGenerations;
|
|
110
62
|
}
|
|
111
63
|
function recordGenerationStart(driver, worker) {
|
|
112
|
-
var ledger = ledgerFor(driver);
|
|
64
|
+
var ledger = ledgerFor(driver, worker);
|
|
113
65
|
var existing = findGeneration(driver, worker);
|
|
114
66
|
if (existing && !existing.endedAt) {
|
|
115
67
|
worker._pairGeneration = existing.generation;
|
|
@@ -184,11 +136,9 @@ function attachPairLifecycle(ctx) {
|
|
|
184
136
|
return record;
|
|
185
137
|
}
|
|
186
138
|
function resolveDriverPair(caller) {
|
|
139
|
+
var args = arguments[1];
|
|
140
|
+
var options = arguments[2];
|
|
187
141
|
if (!caller) throw new Error("pair lifecycle tools require a session-bound tool server");
|
|
188
|
-
// Exact live object identity, before anything is read off the caller. A
|
|
189
|
-
// tool handler captured by a query outlives the session it was bound to, so
|
|
190
|
-
// a stale object — or a different object that happens to carry the same
|
|
191
|
-
// localId — must not be able to act as the Driver.
|
|
192
142
|
if (sm.sessions.get(caller.localId) !== caller) {
|
|
193
143
|
throw new Error("this session is no longer live; the pair tools are bound to an exact session");
|
|
194
144
|
}
|
|
@@ -198,24 +148,25 @@ function attachPairLifecycle(ctx) {
|
|
|
198
148
|
if (!group) throw new Error("this session is not in a split group");
|
|
199
149
|
if (!group.pair) throw new Error("this split group has no Driver/Split Worker roles");
|
|
200
150
|
if (group.pair.driverId !== caller.localId) throw new Error("only the configured Driver can direct this pair");
|
|
201
|
-
var
|
|
202
|
-
|
|
151
|
+
var targetOptions = Object.assign({}, options || { mutation: true }, {
|
|
152
|
+
multiWorkerFeature: ctx.multiWorkerFeature,
|
|
153
|
+
});
|
|
154
|
+
var resolved = pairTarget.resolveGroupTarget(sm, group, caller, args, targetOptions);
|
|
155
|
+
var worker = resolved.partner;
|
|
203
156
|
if ((caller.ownerId || null) !== (worker.ownerId || null)) throw new Error("split partner access denied");
|
|
204
|
-
return { group: group, worker:
|
|
157
|
+
return { group: resolved.group, worker: resolved.partner, role: resolved.role, workerId: resolved.workerId };
|
|
205
158
|
}
|
|
206
|
-
|
|
207
159
|
function replaceBlockedReason(worker) {
|
|
208
160
|
if (worker.isProcessing || worker._queryStarting) return "the Split Worker is mid-turn";
|
|
209
161
|
if (worker._pairDelegation) return "a delegated task is still open";
|
|
210
162
|
return null;
|
|
211
163
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
var resolved = resolveDriverPair(caller);
|
|
164
|
+
function partnerStatus(caller, args) {
|
|
165
|
+
var resolved = resolveDriverPair(caller, args, { readOnly: true });
|
|
215
166
|
var worker = resolved.worker;
|
|
216
167
|
var blocked = turnControl.blockedReason(caller) || replaceBlockedReason(worker);
|
|
217
168
|
var resumeBlocked = turnControl.blockedReason(caller) || (worker.isProcessing || worker._queryStarting ? "the Split Worker is mid-turn" : (!worker._pairInterruption ? "the Split Worker has no interrupted task" : null));
|
|
218
|
-
var ledger = ledgerFor(caller).map(function (record) {
|
|
169
|
+
var ledger = ledgerFor(caller, worker).map(function (record) {
|
|
219
170
|
return {
|
|
220
171
|
generation: record.generation,
|
|
221
172
|
vendor: record.vendor,
|
|
@@ -267,21 +218,49 @@ function attachPairLifecycle(ctx) {
|
|
|
267
218
|
generations: ledger,
|
|
268
219
|
};
|
|
269
220
|
}
|
|
270
|
-
|
|
221
|
+
function sourcePairRegistered(caller, worker) {
|
|
222
|
+
try {
|
|
223
|
+
if (sm.sessions.get(caller.localId) !== caller || sm.sessions.get(worker.localId) !== worker) return false;
|
|
224
|
+
if ((caller.ownerId || null) !== (worker.ownerId || null)) return false;
|
|
225
|
+
var group = store.groupForMember(caller.localId);
|
|
226
|
+
var normalized = splitRoles.normalizePair(group && group.pair, group && group.members);
|
|
227
|
+
return !!(normalized.ok && normalized.kind !== "adhoc" && normalized.driverId === caller.localId &&
|
|
228
|
+
normalized.workerIds.indexOf(worker.localId) !== -1);
|
|
229
|
+
} catch (e) { return false; }
|
|
230
|
+
}
|
|
231
|
+
function restoreSourcePair(ws, caller, worker) {
|
|
232
|
+
try {
|
|
233
|
+
return store.create(ws, {
|
|
234
|
+
members: [caller.localId, worker.localId],
|
|
235
|
+
pair: { driverId: caller.localId, workerId: worker.localId },
|
|
236
|
+
});
|
|
237
|
+
} catch (e) { return { ok: false, error: e.message || String(e) }; }
|
|
238
|
+
}
|
|
239
|
+
function replacementTargetRegistered(caller, oldWorker, sourceWorkerIds) {
|
|
240
|
+
try {
|
|
241
|
+
if (sm.sessions.get(caller.localId) !== caller || sm.sessions.get(oldWorker.localId) !== oldWorker) return null;
|
|
242
|
+
var group = store.groupForMember(caller.localId);
|
|
243
|
+
var normalized = splitRoles.normalizePair(group && group.pair, group && group.members);
|
|
244
|
+
if (!normalized.ok || normalized.kind === "adhoc" || normalized.driverId !== caller.localId) return null;
|
|
245
|
+
var candidates = normalized.workerIds.filter(function (id) { return sourceWorkerIds.indexOf(id) === -1; });
|
|
246
|
+
if (candidates.length !== 1) return null;
|
|
247
|
+
var target = sm.sessions.get(candidates[0]);
|
|
248
|
+
if (!target || sm.sessions.get(target.localId) !== target) return null;
|
|
249
|
+
return (caller.ownerId || null) === (target.ownerId || null) ? target : null;
|
|
250
|
+
} catch (e) { return null; }
|
|
251
|
+
}
|
|
271
252
|
function replacePartner(args, caller) {
|
|
272
|
-
var resolved = resolveDriverPair(caller);
|
|
253
|
+
var resolved = resolveDriverPair(caller, args, { mutation: true });
|
|
273
254
|
var group = resolved.group;
|
|
274
255
|
var oldWorker = resolved.worker;
|
|
256
|
+
var versioned = resolved.role.kind === "versioned";
|
|
257
|
+
var sourceWorkerIds = resolved.role.workerIds.slice();
|
|
275
258
|
ledgerFor(caller);
|
|
276
259
|
turnControl.assertWorkerAction(caller);
|
|
277
260
|
var replacementEntry = replacementState.begin(caller, args.transactionId, oldWorker, replacementFingerprint(args, oldWorker));
|
|
278
261
|
var replay = replacementState.replayValue(replacementEntry);
|
|
279
262
|
if (replay) return Promise.resolve(replay);
|
|
280
263
|
|
|
281
|
-
// Validate the replacement while the old pair is still fully intact. An
|
|
282
|
-
// uninstalled vendor, an unavailable model or an unsupported effort must
|
|
283
|
-
// never cost the user their running Worker, so this precedes the interrupt,
|
|
284
|
-
// the permission cancellation and the dissolve.
|
|
285
264
|
try {
|
|
286
265
|
ctx.preflightWorkerForDriver(caller, {
|
|
287
266
|
workerVendor: args.workerVendor,
|
|
@@ -309,72 +288,77 @@ function attachPairLifecycle(ctx) {
|
|
|
309
288
|
throw validationError;
|
|
310
289
|
}
|
|
311
290
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
ctx.finishDelegation
|
|
291
|
+
var ws = { _clayUser: caller.ownerId ? { id: caller.ownerId } : null };
|
|
292
|
+
try {
|
|
293
|
+
if (blocked) {
|
|
294
|
+
replacementState.stage(replacementEntry.transaction, "stopping_source");
|
|
295
|
+
replacementEntry.transaction.sourceStopped = true;
|
|
296
|
+
if (ctx.markInterruption) ctx.markInterruption(oldWorker, "driver", "The Driver accepted replacement of this Worker generation.");
|
|
297
|
+
oldWorker.taskStopRequested = true;
|
|
298
|
+
if (oldWorker.abortController) {
|
|
299
|
+
try { oldWorker.abortController.abort(); } catch (e) {}
|
|
300
|
+
}
|
|
301
|
+
if (oldWorker._pairDelegation && typeof ctx.finishDelegation === "function") {
|
|
302
|
+
if (typeof ctx.completeInterruptedTask === "function") ctx.completeInterruptedTask(oldWorker, caller, oldWorker._pairDelegation);
|
|
303
|
+
ctx.finishDelegation(group, caller, oldWorker, oldWorker._pairDelegation);
|
|
304
|
+
}
|
|
323
305
|
}
|
|
324
|
-
}
|
|
325
306
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
307
|
+
replacementState.stage(replacementEntry.transaction, "cancelling_source_waits");
|
|
308
|
+
if (typeof ctx.cancelWorkerPermissions === "function") {
|
|
309
|
+
ctx.cancelWorkerPermissions(oldWorker, "The Driver replaced this Split Worker.");
|
|
310
|
+
}
|
|
331
311
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
312
|
+
if (!versioned) {
|
|
313
|
+
replacementState.stage(replacementEntry.transaction, "dissolving_source_pair");
|
|
314
|
+
var dissolved = store.dissolve(ws, { id: group.id });
|
|
315
|
+
if (!dissolved.ok) throw new Error(dissolved.error || "could not dissolve the existing pair");
|
|
316
|
+
}
|
|
317
|
+
} catch (precreateError) {
|
|
318
|
+
var sourceIntact = sourcePairRegistered(caller, oldWorker);
|
|
319
|
+
var precreateRestored = sourceIntact ? { ok: true } : restoreSourcePair(ws, caller, oldWorker);
|
|
320
|
+
var rollback = precreateRestored && precreateRestored.ok ? (sourceIntact ? "safe_retry" : "source_pair_restored") : "source_pair_restore_failed";
|
|
336
321
|
turnControl.releaseCreation(creationTicket);
|
|
337
|
-
replacementState.fail(replacementEntry.transaction, "
|
|
338
|
-
throw
|
|
322
|
+
replacementState.fail(replacementEntry.transaction, "precreate_failed", precreateError, rollback);
|
|
323
|
+
throw precreateError;
|
|
339
324
|
}
|
|
340
325
|
|
|
341
|
-
// History is preserved: the old Worker session stays in the project.
|
|
342
|
-
//
|
|
343
|
-
// Preflight already cleared every input, but the group write itself can
|
|
344
|
-
// still fail, so the dissolve is rolled back rather than leaving the Driver
|
|
345
|
-
// with no pair at all. The restored group is an equivalent record with the
|
|
346
|
-
// same members and roles; its group id is newly issued.
|
|
347
|
-
//
|
|
348
|
-
// What rollback can and cannot undo:
|
|
349
|
-
// - Idle replacement failure is fully recoverable. The Worker session,
|
|
350
|
-
// its history and its still-open ledger generation are all preserved,
|
|
351
|
-
// and the pair is restored.
|
|
352
|
-
// - An explicit interrupt=true is NOT reversible. Stopping a mid-turn
|
|
353
|
-
// Worker aborts its query and cancels the permission decisions it was
|
|
354
|
-
// waiting on; a later creation failure cannot resume that turn. The
|
|
355
|
-
// session and its history survive, but the interrupted work does not
|
|
356
|
-
// come back. The error says so rather than implying a clean restore.
|
|
357
326
|
var created;
|
|
358
327
|
try {
|
|
359
328
|
replacementState.stage(replacementEntry.transaction, "creating_target");
|
|
360
|
-
|
|
329
|
+
var createReplacement = versioned ? ctx.replaceWorkerForDriver : ctx.createWorkerForDriver;
|
|
330
|
+
created = createReplacement(caller, versioned ? oldWorker : {
|
|
361
331
|
workerVendor: typeof args.workerVendor === "string" ? args.workerVendor : "",
|
|
362
332
|
workerModel: typeof args.workerModel === "string" ? args.workerModel : "",
|
|
363
333
|
workerEffort: typeof args.workerEffort === "string" ? args.workerEffort : "",
|
|
364
|
-
}
|
|
334
|
+
}, versioned ? {
|
|
335
|
+
workerVendor: typeof args.workerVendor === "string" ? args.workerVendor : "",
|
|
336
|
+
workerModel: typeof args.workerModel === "string" ? args.workerModel : "",
|
|
337
|
+
workerEffort: typeof args.workerEffort === "string" ? args.workerEffort : "",
|
|
338
|
+
expectedGroup: group,
|
|
339
|
+
expectedWorkerIds: sourceWorkerIds,
|
|
340
|
+
} : undefined);
|
|
365
341
|
} catch (e) {
|
|
366
|
-
var
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
342
|
+
var target = replacementTargetRegistered(caller, oldWorker, sourceWorkerIds);
|
|
343
|
+
if (target) {
|
|
344
|
+
var targetClosed = closeGeneration(caller, oldWorker);
|
|
345
|
+
var targetGeneration = recordGenerationStart(caller, target);
|
|
346
|
+
replacementEntry.transaction.targetWorkerId = target.localId;
|
|
347
|
+
replacementEntry.transaction.targetGeneration = targetGeneration;
|
|
348
|
+
replacementEntry.transaction.previousGeneration = targetClosed ? targetClosed.generation : null;
|
|
349
|
+
replacementState.fail(replacementEntry.transaction, "postcommit_delivery_failed", e, "target_created");
|
|
350
|
+
throw new Error("the replacement Split Worker was created, but its delivery confirmation failed. Reuse Worker " + target.localId + " generation " + targetGeneration + " with send_to_partner; do not replace it again. " + (e.message || String(e)));
|
|
351
|
+
}
|
|
352
|
+
var restored = restoreSourcePair(ws, caller, oldWorker);
|
|
353
|
+
var restoredLive = restored && restored.ok && sourcePairRegistered(caller, oldWorker);
|
|
354
|
+
var restoreNote = restoredLive
|
|
371
355
|
? "The previous pair was restored with its session, history and open generation intact."
|
|
372
356
|
: "The previous pair could not be restored; both sessions are intact and unpaired.";
|
|
373
357
|
var interruptNote = blocked
|
|
374
358
|
? " Its interrupted turn cannot be resumed, because stopping it was explicitly requested."
|
|
375
359
|
: "";
|
|
376
360
|
turnControl.releaseCreation(creationTicket);
|
|
377
|
-
replacementState.fail(replacementEntry.transaction, "creation_failed", e,
|
|
361
|
+
replacementState.fail(replacementEntry.transaction, "creation_failed", e, restoredLive ? "source_pair_restored" : "source_pair_restore_failed");
|
|
378
362
|
throw new Error("could not create the replacement Split Worker: " + (e.message || String(e)) +
|
|
379
363
|
". " + restoreNote + interruptNote);
|
|
380
364
|
}
|
|
@@ -384,7 +368,6 @@ function attachPairLifecycle(ctx) {
|
|
|
384
368
|
if (typeof sm.saveSessionFile === "function") sm.saveSessionFile(caller);
|
|
385
369
|
}
|
|
386
370
|
var generation = recordGenerationStart(caller, created.worker);
|
|
387
|
-
|
|
388
371
|
var result = {
|
|
389
372
|
status: "replaced",
|
|
390
373
|
previousWorkerSessionId: oldWorker.localId,
|
|
@@ -403,14 +386,13 @@ function attachPairLifecycle(ctx) {
|
|
|
403
386
|
|
|
404
387
|
var message = typeof args.message === "string" ? args.message.trim() : "";
|
|
405
388
|
if (!message) return Promise.resolve(result);
|
|
406
|
-
return Promise.resolve(ctx.sendToPartner({ message: message, wait: args.wait, timeoutSeconds: args.timeoutSeconds }, caller))
|
|
389
|
+
return Promise.resolve(ctx.sendToPartner({ message: message, wait: args.wait, timeoutSeconds: args.timeoutSeconds, workerId: created.worker.localId }, caller))
|
|
407
390
|
.then(function (delivered) {
|
|
408
391
|
result.delivery = delivered;
|
|
409
392
|
return result;
|
|
410
393
|
});
|
|
411
394
|
}
|
|
412
395
|
|
|
413
|
-
// No global or cross-user ranking is formed from Worker evaluations.
|
|
414
396
|
function validateEvaluation(raw) {
|
|
415
397
|
var input = typeof raw === "string" ? { outcome: raw } : (raw && typeof raw === "object" ? raw : {});
|
|
416
398
|
var outcome = typeof input.outcome === "string" ? input.outcome.trim().toLowerCase() : "";
|
|
@@ -430,8 +412,8 @@ function attachPairLifecycle(ctx) {
|
|
|
430
412
|
}
|
|
431
413
|
|
|
432
414
|
function recordEvaluation(args, caller) {
|
|
433
|
-
var resolved = resolveDriverPair(caller);
|
|
434
|
-
var ledger = ledgerFor(caller);
|
|
415
|
+
var resolved = resolveDriverPair(caller, args, { mutation: true });
|
|
416
|
+
var ledger = ledgerFor(caller, resolved.worker);
|
|
435
417
|
var target = Number.isInteger(args.generation)
|
|
436
418
|
? (function () {
|
|
437
419
|
for (var i = 0; i < ledger.length; i++) {
|
|
@@ -453,14 +435,14 @@ function attachPairLifecycle(ctx) {
|
|
|
453
435
|
};
|
|
454
436
|
}
|
|
455
437
|
|
|
456
|
-
function optionalStatus(boundSession) {
|
|
457
|
-
try { return partnerStatus(boundSession); } catch (e) { return null; }
|
|
438
|
+
function optionalStatus(boundSession, args) {
|
|
439
|
+
try { return partnerStatus(boundSession, args); } catch (e) { return null; }
|
|
458
440
|
}
|
|
459
441
|
|
|
460
442
|
function toolHandlers(boundSession) {
|
|
461
443
|
return {
|
|
462
|
-
status: function () {
|
|
463
|
-
try { return toolResult(partnerStatus(boundSession)); }
|
|
444
|
+
status: function (args) {
|
|
445
|
+
try { return toolResult(partnerStatus(boundSession, args || {})); }
|
|
464
446
|
catch (e) { return toolError(e.message || String(e)); }
|
|
465
447
|
},
|
|
466
448
|
replace: function (args) {
|
|
@@ -17,7 +17,7 @@ function attachPairMessage(ctx) {
|
|
|
17
17
|
if (!id) return toolResult({ status: "rejected", requestId: null, reason: "requestId is invalid" });
|
|
18
18
|
try {
|
|
19
19
|
ctx.turnControl.assertWorkerAction(caller);
|
|
20
|
-
var resolved = ctx.resolvePair(caller);
|
|
20
|
+
var resolved = ctx.resolvePair(caller, args, { mutation: true });
|
|
21
21
|
var worker = resolved.partner;
|
|
22
22
|
var message = typeof args.message === "string" ? args.message.trim() : "";
|
|
23
23
|
if (!message) return toolResult({ status: "rejected", requestId: id, partnerId: worker.localId, reason: "message is required" });
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
function stopOwnedPair(session, roles, turnControl, taskControl, workerPermission, reason) {
|
|
2
2
|
if (!roles) return false;
|
|
3
3
|
turnControl.markHumanStop(session);
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
var workers = roles.workers || [roles.worker];
|
|
5
|
+
for (var wi = 0; wi < workers.length; wi++) {
|
|
6
|
+
taskControl.cancelAll(workers[wi]);
|
|
7
|
+
taskControl.markInterruption(workers[wi], "user", reason || "The human stopped this scheduled run.");
|
|
8
|
+
workerPermission.cancelForSession(workers[wi], reason || "The human stopped this scheduled run.");
|
|
9
|
+
}
|
|
10
|
+
var members = [roles.driver].concat(workers);
|
|
8
11
|
for (var i = 0; i < members.length; i++) {
|
|
9
12
|
members[i].taskStopRequested = true;
|
|
10
13
|
if (members[i].queryInstance && typeof members[i].queryInstance.close === "function") {
|
|
@@ -46,7 +46,11 @@ function fail(transaction, stageName, error, rollback) {
|
|
|
46
46
|
transaction.stage = stageName;
|
|
47
47
|
transaction.updatedAt = Date.now();
|
|
48
48
|
transaction.completedAt = transaction.updatedAt;
|
|
49
|
-
transaction.failure = {
|
|
49
|
+
transaction.failure = {
|
|
50
|
+
message: error && (error.message || String(error)) || "Unknown replacement failure",
|
|
51
|
+
rollback: rollback || "not_required",
|
|
52
|
+
retryable: rollback === "safe_retry" || (stageName === "creation_failed" && rollback === "source_pair_restored"),
|
|
53
|
+
};
|
|
50
54
|
transaction.sourcePairRestored = rollback === "source_pair_restored" ? true : (rollback === "source_pair_restore_failed" ? false : null);
|
|
51
55
|
return transaction;
|
|
52
56
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
function attachPairResultCapture(ctx) {
|
|
2
|
+
var splitRoles = require("./session-split-group-roles");
|
|
3
|
+
function current(caller, partner, token) {
|
|
4
|
+
var group = ctx.store.groupForMember(partner.localId);
|
|
5
|
+
return !!(group && group.id === token.groupId && partner._pairDelegation === token &&
|
|
6
|
+
splitRoles.matchesLiveTarget(ctx.sm, group, caller, partner) && splitRoles.tokenMatchesWorker(partner, token));
|
|
7
|
+
}
|
|
8
|
+
function begin(caller, partner, token, message, projectSlug) {
|
|
9
|
+
if (!ctx.resultOutbox) return { ok: true };
|
|
10
|
+
var driverOriginId = ctx.resultOutbox.stableOrigin(caller), workerOriginId = ctx.resultOutbox.stableOrigin(partner);
|
|
11
|
+
if (!driverOriginId || !workerOriginId) return { ok: false, error: "durable session origins are required before dispatch" };
|
|
12
|
+
var started = ctx.resultOutbox.begin({ ownerId: caller.ownerId, projectSlug: projectSlug,
|
|
13
|
+
driverOriginId: driverOriginId, workerOriginId: workerOriginId,
|
|
14
|
+
taskId: token.taskId, generation: token.generation, driverSessionId: caller.localId,
|
|
15
|
+
workerSessionId: partner.localId, groupId: token.groupId,
|
|
16
|
+
historyStartIndex: token.startIndex, message: message,
|
|
17
|
+
deliveryRoute: token.wait === true ? "blocking" : "callback" });
|
|
18
|
+
if (started.ok) token.outboxKey = started.key;
|
|
19
|
+
return started;
|
|
20
|
+
}
|
|
21
|
+
function finish(caller, partner, token, delivered) {
|
|
22
|
+
var group = ctx.store.groupForMember(partner.localId);
|
|
23
|
+
if (token._captureFinished || !current(caller, partner, token)) return false;
|
|
24
|
+
token._captureFinished = true;
|
|
25
|
+
try {
|
|
26
|
+
if (typeof ctx.onPartnerResult === "function") ctx.onPartnerResult(caller, partner, token);
|
|
27
|
+
if (token.outboxKey && ctx.resultOutbox) {
|
|
28
|
+
var finalized = ctx.resultOutbox.markFinalized(token.outboxKey);
|
|
29
|
+
if (!finalized.ok) throw new Error(finalized.error || "completed result finalization could not be saved");
|
|
30
|
+
}
|
|
31
|
+
ctx.finishDelegation(group, caller, partner, token);
|
|
32
|
+
if (typeof ctx.onStateChange === "function" && token.outboxKey) ctx.onStateChange(caller, ctx.resultOutbox.get(token.outboxKey));
|
|
33
|
+
} catch (error) {
|
|
34
|
+
token._captureFinished = false;
|
|
35
|
+
token._capturePending = true;
|
|
36
|
+
token._captureHookError = error.message || String(error);
|
|
37
|
+
if (token.outboxKey && ctx.resultOutbox) {
|
|
38
|
+
var failed = ctx.resultOutbox.markFinalizationFailure(token.outboxKey, token._captureHookError);
|
|
39
|
+
if (typeof ctx.onStateChange === "function") ctx.onStateChange(caller, Object.assign({}, failed.record || ctx.resultOutbox.get(token.outboxKey), {
|
|
40
|
+
finalizationState: "failed", finalizationError: token._captureHookError, persistenceError: failed.ok ? null : failed.error,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
delete token._capturePending;
|
|
46
|
+
var resumed = delivered ? true : ctx.resumeDriverWithResult(caller, partner, token);
|
|
47
|
+
if (!partner._pairClosing) ctx.drain(caller, partner);
|
|
48
|
+
return resumed;
|
|
49
|
+
}
|
|
50
|
+
function capture(caller, partner, token, outcome) {
|
|
51
|
+
if (!ctx.resultOutbox || !token.outboxKey) return finish(caller, partner, token);
|
|
52
|
+
if (token._captureInProgress || token._captureComplete) return false;
|
|
53
|
+
token._captureInProgress = true;
|
|
54
|
+
var saved;
|
|
55
|
+
try {
|
|
56
|
+
saved = ctx.resultOutbox.capture(token.outboxKey, outcome, function (retryResult) {
|
|
57
|
+
token._captureInProgress = false;
|
|
58
|
+
if (retryResult && retryResult.ok) {
|
|
59
|
+
token._captureComplete = true;
|
|
60
|
+
var capturedRecord = ctx.resultOutbox.get(token.outboxKey);
|
|
61
|
+
if (typeof ctx.onStateChange === "function") ctx.onStateChange(caller, capturedRecord);
|
|
62
|
+
if (capturedRecord && capturedRecord.deliveryRoute === "callback") deliver(caller, partner, token);
|
|
63
|
+
else finish(caller, partner, token);
|
|
64
|
+
}
|
|
65
|
+
}, function () { return current(caller, partner, token); });
|
|
66
|
+
} catch (error) {
|
|
67
|
+
token._captureInProgress = false;
|
|
68
|
+
token._capturePending = true;
|
|
69
|
+
token._captureHookError = error.message || String(error);
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (saved.ok) { token._captureInProgress = false; token._captureComplete = true; }
|
|
73
|
+
if (saved.ok) {
|
|
74
|
+
var capturedRecord = ctx.resultOutbox.get(token.outboxKey);
|
|
75
|
+
if (typeof ctx.onStateChange === "function") ctx.onStateChange(caller, capturedRecord);
|
|
76
|
+
var delivered = capturedRecord && capturedRecord.deliveryRoute === "callback" ? deliver(caller, partner, token) : finish(caller, partner, token);
|
|
77
|
+
return delivered === false && token._capturePending ? false : delivered;
|
|
78
|
+
}
|
|
79
|
+
token._capturePending = true;
|
|
80
|
+
if (!token._captureFailureReported) {
|
|
81
|
+
token._captureFailureReported = true;
|
|
82
|
+
try { ctx.sm.sendAndRecord(caller, { type: "error", text: "The Split Worker completed, but its result could not be durably captured yet. The task remains retained for bounded server retry." }); } catch (error) { token._captureNoticeError = error.message || String(error); }
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
function prepare(caller, partner, token, outcome) {
|
|
87
|
+
if (!ctx.resultOutbox || !token.outboxKey) return { ok: true, prepared: false };
|
|
88
|
+
var saved = ctx.resultOutbox.prepareCapture(token.outboxKey, outcome);
|
|
89
|
+
return saved.ok ? { ok: true, prepared: true, duplicate: !!saved.duplicate } : saved;
|
|
90
|
+
}
|
|
91
|
+
function rollbackPrepared(token, outcome) {
|
|
92
|
+
if (!ctx.resultOutbox || !token.outboxKey) return { ok: true };
|
|
93
|
+
return ctx.resultOutbox.rollbackPreparedCapture(token.outboxKey, outcome);
|
|
94
|
+
}
|
|
95
|
+
function finalizePrepared(token, outcome) {
|
|
96
|
+
if (!ctx.resultOutbox || !token.outboxKey) return { ok: true, record: null };
|
|
97
|
+
return ctx.resultOutbox.finalizePreparedCapture(token.outboxKey, outcome);
|
|
98
|
+
}
|
|
99
|
+
function acceptPrepared(caller, partner, token) {
|
|
100
|
+
if (ctx.resultOutbox && token.outboxKey) {
|
|
101
|
+
token._captureComplete = true;
|
|
102
|
+
var record = ctx.resultOutbox.get(token.outboxKey);
|
|
103
|
+
if (typeof ctx.onStateChange === "function") ctx.onStateChange(caller, record);
|
|
104
|
+
return record && record.deliveryRoute === "callback" ? deliver(caller, partner, token) : finish(caller, partner, token);
|
|
105
|
+
}
|
|
106
|
+
return finish(caller, partner, token);
|
|
107
|
+
}
|
|
108
|
+
function deliver(caller, partner, token) {
|
|
109
|
+
if (!ctx.delivery) return finish(caller, partner, token);
|
|
110
|
+
return ctx.delivery.wake(caller, partner, token);
|
|
111
|
+
}
|
|
112
|
+
return { begin: begin, capture: capture, prepare: prepare, finalizePrepared: finalizePrepared, rollbackPrepared: rollbackPrepared,
|
|
113
|
+
acceptPrepared: acceptPrepared, deliver: deliver, finish: finish };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = { attachPairResultCapture: attachPairResultCapture };
|