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