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
|
@@ -4,32 +4,42 @@
|
|
|
4
4
|
// the size limit and so the guidance can be reviewed as prose.
|
|
5
5
|
|
|
6
6
|
var WORKER_RUNTIME_SELECTION = [
|
|
7
|
-
"Use a
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"runtime choice; this is a recommendation policy, not a restriction on which model the user may select.",
|
|
7
|
+
"Use a role-based mix by default: keep decomposition, judgment, review, and independent verification with",
|
|
8
|
+
"the Driver, and give the Split Worker concrete, bounded execution tasks with clear acceptance criteria.",
|
|
9
|
+
"Recommend the lowest-capability model and thinking effort that are adequate for the specific task, based on",
|
|
10
|
+
"the requirements, the server-provided capability evidence, the offered runtime catalog, and recorded local",
|
|
11
|
+
"Worker outcomes. Choose vendor, model, and effort separately. Do not mirror the Driver, prefer a vendor by",
|
|
12
|
+
"Explicitly fill the vendor, model, and thinking effort recommendation fields for both creation and replacement;",
|
|
13
|
+
"never rely on card defaults or leave model selection implicit.",
|
|
14
|
+
"brand, or default to the strongest available option. Do not invent availability, prices, rankings, or missing",
|
|
15
|
+
"metadata. If the catalog or capability evidence is incomplete, say what is unknown and choose only from what",
|
|
16
|
+
"is actually offered. Honor an explicit user runtime choice; this is guidance, not a restriction.",
|
|
17
|
+
"Escalate only for a concrete task requirement or an observed failure the current runtime cannot resolve, and",
|
|
18
|
+
"state the evidence. A small review correction is not by itself an escalation reason; send one consolidated",
|
|
19
|
+
"bounded correction to the Worker and verify the result. Repeated same-class failures require reassessing the",
|
|
20
|
+
"task contract, model, and effort with evidence, not endless cheap-model loops or escalation of every correction.",
|
|
21
|
+
"Aim for a role-based workload direction near 30 percent Driver and 70 percent Worker effort when useful, but",
|
|
22
|
+
"never promise that ratio or treat it as a quota, token, or cost guarantee, and never pad work to reach it.",
|
|
24
23
|
].join(" ").replace(/ {2,}/g, " ");
|
|
25
24
|
|
|
26
25
|
var DRIVER_DELEGATION = [
|
|
27
|
-
"Your management objective: protect your own context, keep the Split Worker compact, and put execution where",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
26
|
+
"Your management objective: protect your own context, keep the Split Worker compact, and put substantive execution where it runs best.",
|
|
27
|
+
"Delegate implementation-heavy work rather than doing it here. Treat work spanning multiple modules, crossing",
|
|
28
|
+
"client/server/data boundaries, requiring independent investigation, testing, or self-correction as implementation-heavy.",
|
|
29
|
+
"Your own capability is not a reason to retain that execution: preserve the Driver for decomposition,",
|
|
30
|
+
"runtime selection, judgment, review, and integration unless the user explicitly asks you to work directly or the",
|
|
31
|
+
"work must remain sequential in this session.",
|
|
32
|
+
"Give each Worker one bounded contract: goal and scope, constraints and non-goals, acceptance commands and the",
|
|
33
|
+
"expected behaviors, and the required handoff. Delegate substantive investigation, implementation, testing, and",
|
|
34
|
+
"self-correction. The Worker must finish its allowed scope, inspect its diff against the criteria, run meaningful",
|
|
35
|
+
"production-path tests, and correct failures before reporting. It must not hand back an unchecked draft. Require",
|
|
36
|
+
"a bounded self-correction attempt; when progress is not possible, require a precise blocker and evidence.",
|
|
37
|
+
"Review once per coherent completed milestone with risk-focused source review and independent verification. Do not",
|
|
38
|
+
"rubber-stamp or sacrifice quality for an efficiency direction. Consolidate review feedback into one correction",
|
|
39
|
+
"task rather than sending overlapping live and queued work.",
|
|
40
|
+
"When discussing measured efficiency, distinguish requests, input tokens, output tokens, cached tokens, and cost.",
|
|
41
|
+
"Unknown usage is not zero; the 30/70 direction is role-based and aspirational, not a guaranteed token or cost",
|
|
42
|
+
"ratio or quota.",
|
|
33
43
|
].join(" ").replace(/ {2,}/g, " ");
|
|
34
44
|
|
|
35
45
|
var DRIVER_CORE = [
|
|
@@ -55,6 +65,11 @@ var DRIVER_CORE = [
|
|
|
55
65
|
"interrupt true, which stops it first only after acceptance. A human Stop",
|
|
56
66
|
"is authoritative: do not retry, send more work, or replace the Worker in the same turn. Clay blocks those",
|
|
57
67
|
"actions until the human sends a new Driver message. Use close_partner when they ask to close the pane.",
|
|
68
|
+
"If replacement fails before a new Worker is created, retry only when the transaction explicitly confirms a safe",
|
|
69
|
+
"rollback; still respect the failed-attempt cap and the human Stop barrier. If a new Worker was created but its task or transport",
|
|
70
|
+
"failed, keep that Worker and use send_to_partner for a follow-up; do not replace it again merely because execution",
|
|
71
|
+
"failed. True replacement quota exhaustion remains bounded and honest, and a human Stop always requires a new",
|
|
72
|
+
"human message.",
|
|
58
73
|
"A Worker outcome envelope is Worker-reported evidence, not Driver verification. Preserve partial results and",
|
|
59
74
|
"correlate completions to their task and generation; never attach a late old-generation result to a new Worker.",
|
|
60
75
|
"",
|
|
@@ -71,7 +86,13 @@ var DRIVER_CORE = [
|
|
|
71
86
|
"ambiguous and a visible pair exists, prefer the visible Split Worker. If review or user feedback requires",
|
|
72
87
|
"corrections to the Split Worker's implementation, delegate a follow-up turn to that Split Worker instead of",
|
|
73
88
|
"editing its files yourself. If a non-waiting delegation finishes later, Clay pushes the result back and",
|
|
74
|
-
"resumes you automatically.
|
|
89
|
+
"resumes you automatically. send_to_partner returns immediately by default; set wait to true only when you",
|
|
90
|
+
"explicitly need the final result in the same tool call. Do useful independent work after a non-waiting",
|
|
91
|
+
"delegation, then yield for Clay's automatic completion callback; do not poll read_partner or partner_status,",
|
|
92
|
+
"or sleep while waiting. Use those reads only for a user-requested status, concrete failure or stall diagnosis,",
|
|
93
|
+
"capacity/reuse decision, or exact queue inspection before mutation. Do not send the same correction as both",
|
|
94
|
+
"a live message and a queued task; consolidate review feedback into one bounded follow-up. You also decide",
|
|
95
|
+
"the Split Worker's tool-permission requests when they arise;",
|
|
75
96
|
"approve only what falls inside the task the user authorized. Integrate and verify the final outcome. Do not",
|
|
76
97
|
"search the project for implementations of these tools, and do not delegate work that must be performed",
|
|
77
98
|
"sequentially in this same session.",
|
|
@@ -80,7 +101,7 @@ var DRIVER_CORE = [
|
|
|
80
101
|
var DRIVER = DRIVER_CORE + " " + DRIVER_DELEGATION;
|
|
81
102
|
|
|
82
103
|
function worker(taskId) {
|
|
83
|
-
return "You are the configured Split Worker. For task " + (taskId || "unknown") + ",
|
|
104
|
+
return "You are the configured Split Worker. For task " + (taskId || "unknown") + ", finish only the allowed bounded scope. Inspect your diff against the stated acceptance criteria, run meaningful production-path tests, and correct your own failures within a bounded self-correction attempt before reporting. Do not hand back an unchecked draft or hide a failure; if progress is blocked, report the precise blocker and evidence. Call report_partner_outcome before finishing when the tool is available. Report only what you observed: changed files and behavior, commands with actual exit results, known limits or unverified work, running processes, and next action. Never claim Driver verification.";
|
|
84
105
|
}
|
|
85
106
|
|
|
86
107
|
var UNPAIRED = [
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
var splitRoles = require("./session-split-group-roles");
|
|
2
|
+
var multiWorkerFeature = require("./multi-worker-feature");
|
|
3
|
+
|
|
4
|
+
function resolveGroupTarget(sm, group, caller, args, options) {
|
|
5
|
+
if (!caller || sm.sessions.get(caller.localId) !== caller) throw new Error("this session is no longer live; the partner tools are bound to an exact session");
|
|
6
|
+
var requestedWorkerId = args && args.workerId;
|
|
7
|
+
var normalized = splitRoles.normalizePair(group && group.pair, group && group.members);
|
|
8
|
+
if (!normalized.ok) throw new Error(normalized.error);
|
|
9
|
+
var workerId;
|
|
10
|
+
if (normalized.kind === "adhoc") {
|
|
11
|
+
workerId = group.members[0] === caller.localId ? group.members[1] : group.members[0];
|
|
12
|
+
if (requestedWorkerId != null && requestedWorkerId !== workerId) throw new Error("workerId must identify the exact split partner");
|
|
13
|
+
} else {
|
|
14
|
+
if (normalized.driverId !== caller.localId) throw new Error("only the configured Driver can direct this pair");
|
|
15
|
+
var target = splitRoles.resolveWorkerTarget(normalized, requestedWorkerId);
|
|
16
|
+
if (!target.ok) throw new Error(target.error);
|
|
17
|
+
workerId = target.workerId;
|
|
18
|
+
if (normalized.kind === "versioned" && (!options || options.readOnly !== true) &&
|
|
19
|
+
(!options || !multiWorkerFeature.isEnabled(options.multiWorkerFeature))) {
|
|
20
|
+
throw new Error("multi-Worker mutations remain gated until lifecycle, result, and Stop routing are migrated");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
var worker = sm.sessions.get(workerId);
|
|
24
|
+
if (!worker || sm.sessions.get(worker.localId) !== worker) throw new Error("split partner session was not found");
|
|
25
|
+
if ((caller.ownerId || null) !== (worker.ownerId || null)) throw new Error("split partner access denied");
|
|
26
|
+
if (group.members.indexOf(caller.localId) === -1 || group.members.indexOf(worker.localId) === -1) {
|
|
27
|
+
throw new Error("the exact caller and Worker must both belong to this split group");
|
|
28
|
+
}
|
|
29
|
+
return { group: group, partner: worker, role: normalized, workerId: workerId };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function rolesFor(sm, store, session) {
|
|
33
|
+
if (!session || sm.sessions.get(session.localId) !== session) return null;
|
|
34
|
+
var group = store.groupForMember(session.localId);
|
|
35
|
+
var roles = group && group.pair ? splitRoles.resolveLiveRoles(sm, group) : null;
|
|
36
|
+
if (!roles) return null;
|
|
37
|
+
roles.worker = roles.workers.length === 1 ? roles.workers[0] :
|
|
38
|
+
roles.workers.find(function (worker) { return worker === session; }) || null;
|
|
39
|
+
return roles;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { resolveGroupTarget: resolveGroupTarget, rolesFor: rolesFor };
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
var MAX_CREATIONS_PER_TURN = 2;
|
|
10
10
|
var MAX_REPLACEMENTS_PER_TURN = 1;
|
|
11
|
+
var MAX_FAILED_REPLACEMENT_ATTEMPTS_PER_TURN = 2;
|
|
11
12
|
var MAX_OPERATION_ID_CHARS = 120;
|
|
13
|
+
var splitRoles = require("./session-split-group-roles");
|
|
12
14
|
|
|
13
15
|
function attachPairTurnControl(ctx) {
|
|
14
16
|
var sm = ctx.sm;
|
|
@@ -21,11 +23,16 @@ function attachPairTurnControl(ctx) {
|
|
|
21
23
|
humanStopped: false,
|
|
22
24
|
stoppedAt: null,
|
|
23
25
|
stoppedWorkerId: null,
|
|
26
|
+
stoppedWorkerIds: [],
|
|
24
27
|
creations: 0,
|
|
25
28
|
replacements: 0,
|
|
29
|
+
failedReplacementAttempts: 0,
|
|
26
30
|
operations: Object.create(null),
|
|
27
31
|
};
|
|
28
32
|
}
|
|
33
|
+
if (!Number.isFinite(driver._pairTurnControl.failedReplacementAttempts)) {
|
|
34
|
+
driver._pairTurnControl.failedReplacementAttempts = 0;
|
|
35
|
+
}
|
|
29
36
|
return driver._pairTurnControl;
|
|
30
37
|
}
|
|
31
38
|
|
|
@@ -37,11 +44,11 @@ function attachPairTurnControl(ctx) {
|
|
|
37
44
|
if (!liveSession(session)) return null;
|
|
38
45
|
var group = store.groupForMember(session.localId);
|
|
39
46
|
if (!group || !group.pair) return null;
|
|
40
|
-
var
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return
|
|
47
|
+
var roles = splitRoles.resolveLiveRoles(sm, group);
|
|
48
|
+
if (!roles) return null;
|
|
49
|
+
roles.worker = roles.workers.length === 1 ? roles.workers[0] :
|
|
50
|
+
roles.workers.find(function (worker) { return worker === session; }) || null;
|
|
51
|
+
return roles;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
function beginHumanTurn(session) {
|
|
@@ -54,8 +61,10 @@ function attachPairTurnControl(ctx) {
|
|
|
54
61
|
state.humanStopped = false;
|
|
55
62
|
state.stoppedAt = null;
|
|
56
63
|
state.stoppedWorkerId = null;
|
|
64
|
+
state.stoppedWorkerIds = [];
|
|
57
65
|
state.creations = 0;
|
|
58
66
|
state.replacements = 0;
|
|
67
|
+
state.failedReplacementAttempts = 0;
|
|
59
68
|
state.operations = Object.create(null);
|
|
60
69
|
return true;
|
|
61
70
|
}
|
|
@@ -66,7 +75,8 @@ function attachPairTurnControl(ctx) {
|
|
|
66
75
|
var state = stateFor(roles.driver);
|
|
67
76
|
state.humanStopped = true;
|
|
68
77
|
state.stoppedAt = Date.now();
|
|
69
|
-
state.stoppedWorkerId = roles.worker.localId;
|
|
78
|
+
state.stoppedWorkerId = roles.worker ? roles.worker.localId : null;
|
|
79
|
+
state.stoppedWorkerIds = roles.workers.map(function (worker) { return worker.localId; });
|
|
70
80
|
return roles;
|
|
71
81
|
}
|
|
72
82
|
|
|
@@ -88,20 +98,28 @@ function attachPairTurnControl(ctx) {
|
|
|
88
98
|
if (kind === "replace" && state.replacements >= MAX_REPLACEMENTS_PER_TURN) {
|
|
89
99
|
throw new Error("the Split Worker replacement limit for this human turn has been reached; wait for a new human message before replacing it again");
|
|
90
100
|
}
|
|
101
|
+
if (kind === "replace" && state.failedReplacementAttempts >= MAX_FAILED_REPLACEMENT_ATTEMPTS_PER_TURN) {
|
|
102
|
+
throw new Error("the Split Worker replacement failure limit for this human turn has been reached; wait for a new human message before retrying replacement");
|
|
103
|
+
}
|
|
91
104
|
if (state.creations >= MAX_CREATIONS_PER_TURN) {
|
|
92
105
|
throw new Error("the Split Worker creation limit for this human turn has been reached; wait for a new human message before creating another Worker");
|
|
93
106
|
}
|
|
94
107
|
state.creations += 1;
|
|
95
108
|
if (kind === "replace") state.replacements += 1;
|
|
96
|
-
return { driver: driver, kind: kind, active: true };
|
|
109
|
+
return { driver: driver, kind: kind, turnSerial: state.serial, active: true };
|
|
97
110
|
}
|
|
98
111
|
|
|
99
112
|
function releaseCreation(ticket) {
|
|
100
|
-
if (!ticket || !ticket.active
|
|
113
|
+
if (!ticket || !ticket.active) return;
|
|
101
114
|
ticket.active = false;
|
|
115
|
+
if (!liveSession(ticket.driver)) return;
|
|
102
116
|
var state = stateFor(ticket.driver);
|
|
117
|
+
if (state.serial !== ticket.turnSerial) return;
|
|
103
118
|
state.creations = Math.max(0, state.creations - 1);
|
|
104
|
-
if (ticket.kind === "replace")
|
|
119
|
+
if (ticket.kind === "replace") {
|
|
120
|
+
state.replacements = Math.max(0, state.replacements - 1);
|
|
121
|
+
state.failedReplacementAttempts += 1;
|
|
122
|
+
}
|
|
105
123
|
}
|
|
106
124
|
|
|
107
125
|
function operationId(value) {
|
|
@@ -133,9 +151,11 @@ function attachPairTurnControl(ctx) {
|
|
|
133
151
|
humanStopped: state.humanStopped,
|
|
134
152
|
stoppedAt: state.stoppedAt,
|
|
135
153
|
stoppedWorkerId: state.stoppedWorkerId,
|
|
154
|
+
stoppedWorkerIds: state.stoppedWorkerIds.slice(),
|
|
136
155
|
turnSerial: state.serial,
|
|
137
156
|
creationsThisTurn: state.creations,
|
|
138
157
|
replacementsThisTurn: state.replacements,
|
|
158
|
+
failedReplacementAttemptsThisTurn: state.failedReplacementAttempts,
|
|
139
159
|
};
|
|
140
160
|
}
|
|
141
161
|
|
|
@@ -154,5 +174,6 @@ function attachPairTurnControl(ctx) {
|
|
|
154
174
|
module.exports = {
|
|
155
175
|
MAX_CREATIONS_PER_TURN: MAX_CREATIONS_PER_TURN,
|
|
156
176
|
MAX_REPLACEMENTS_PER_TURN: MAX_REPLACEMENTS_PER_TURN,
|
|
177
|
+
MAX_FAILED_REPLACEMENT_ATTEMPTS_PER_TURN: MAX_FAILED_REPLACEMENT_ATTEMPTS_PER_TURN,
|
|
157
178
|
attachPairTurnControl: attachPairTurnControl,
|
|
158
179
|
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
function value(input) {
|
|
2
|
+
return typeof input === "string" && input ? input : null;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
var splitRoles = require("./session-split-group-roles");
|
|
6
|
+
|
|
7
|
+
function pairFor(session, previousCli, previousOrigin) {
|
|
8
|
+
return {
|
|
9
|
+
cli: value(session && session.cliSessionId) || value(previousCli),
|
|
10
|
+
origin: value(session && session.sessionOriginId) || value(previousOrigin),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function versionedAnchor(session) {
|
|
15
|
+
var cli = value(session && session.cliSessionId);
|
|
16
|
+
var origin = value(session && session.sessionOriginId);
|
|
17
|
+
if (!cli && !origin) return null;
|
|
18
|
+
return { cli: cli, origin: origin };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function versionedPairAnchors(group, sessions) {
|
|
22
|
+
var normalized = splitRoles.normalizePair(group && group.pair, group && group.members);
|
|
23
|
+
if (!normalized.ok || normalized.kind !== "versioned") return null;
|
|
24
|
+
var driver = versionedAnchor(sessions.get(group.pair.driverId));
|
|
25
|
+
if (!driver) return null;
|
|
26
|
+
var workers = [];
|
|
27
|
+
for (var i = 0; i < normalized.workerIds.length; i++) {
|
|
28
|
+
var worker = versionedAnchor(sessions.get(normalized.workerIds[i]));
|
|
29
|
+
if (!worker) return null;
|
|
30
|
+
workers.push(worker);
|
|
31
|
+
}
|
|
32
|
+
return { version: 2, driver: driver, workers: workers };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function versionedMemberAnchors(group, sessions) {
|
|
36
|
+
var normalized = splitRoles.normalizePair(group && group.pair, group && group.members);
|
|
37
|
+
if (!normalized.ok || normalized.kind !== "versioned") return null;
|
|
38
|
+
var members = [];
|
|
39
|
+
for (var i = 0; i < group.members.length; i++) {
|
|
40
|
+
var anchor = versionedAnchor(sessions.get(group.members[i]));
|
|
41
|
+
if (!anchor) return null;
|
|
42
|
+
members.push(anchor);
|
|
43
|
+
}
|
|
44
|
+
return { version: 2, members: members };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveVersionedPair(sessions, anchors) {
|
|
48
|
+
if (!anchors || anchors.version !== 2 || !anchors.driver || !Array.isArray(anchors.workers) ||
|
|
49
|
+
(anchors.workers.length !== 1 && anchors.workers.length !== 2)) return null;
|
|
50
|
+
var driver = findSession(sessions, anchors.driver.cli, anchors.driver.origin);
|
|
51
|
+
if (!driver) return null;
|
|
52
|
+
var workers = [];
|
|
53
|
+
for (var i = 0; i < anchors.workers.length; i++) {
|
|
54
|
+
var workerAnchor = anchors.workers[i];
|
|
55
|
+
if (!workerAnchor || (!value(workerAnchor.cli) && !value(workerAnchor.origin))) return null;
|
|
56
|
+
var worker = workerAnchor && findSession(sessions, workerAnchor.cli, workerAnchor.origin);
|
|
57
|
+
if (!worker || workers.indexOf(worker) !== -1 || worker === driver) return null;
|
|
58
|
+
workers.push(worker);
|
|
59
|
+
}
|
|
60
|
+
return { driver: driver, workers: workers };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveVersionedMembers(sessions, anchors) {
|
|
64
|
+
if (!anchors || anchors.version !== 2 || !Array.isArray(anchors.members) ||
|
|
65
|
+
(anchors.members.length !== 2 && anchors.members.length !== 3)) return null;
|
|
66
|
+
var resolved = [];
|
|
67
|
+
for (var i = 0; i < anchors.members.length; i++) {
|
|
68
|
+
var anchor = anchors.members[i];
|
|
69
|
+
if (!anchor || (!value(anchor.cli) && !value(anchor.origin))) return null;
|
|
70
|
+
var session = anchor && findSession(sessions, anchor.cli, anchor.origin);
|
|
71
|
+
if (!session || resolved.indexOf(session) !== -1) return null;
|
|
72
|
+
resolved.push(session);
|
|
73
|
+
}
|
|
74
|
+
return resolved;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function memberAnchors(group, sessions) {
|
|
78
|
+
var normalized = splitRoles.normalizePair(group && group.pair, group && group.members);
|
|
79
|
+
if (!normalized.ok) return null;
|
|
80
|
+
if (normalized.kind === "versioned") return versionedMemberAnchors(group, sessions);
|
|
81
|
+
var previousCli = Array.isArray(group.memberCliIds) ? group.memberCliIds : [];
|
|
82
|
+
var previousOrigin = Array.isArray(group.memberOriginIds) ? group.memberOriginIds : [];
|
|
83
|
+
var cli = [], origin = [];
|
|
84
|
+
for (var i = 0; i < group.members.length; i++) {
|
|
85
|
+
var anchor = pairFor(sessions.get(group.members[i]), previousCli[i], previousOrigin[i]);
|
|
86
|
+
cli.push(anchor.cli); origin.push(anchor.origin);
|
|
87
|
+
}
|
|
88
|
+
return { cli: cli, origin: origin };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function pairAnchors(group, sessions) {
|
|
92
|
+
var normalized = splitRoles.normalizePair(group && group.pair, group && group.members);
|
|
93
|
+
if (!normalized.ok || normalized.kind === "adhoc") return null;
|
|
94
|
+
if (normalized.kind === "versioned") return versionedPairAnchors(group, sessions);
|
|
95
|
+
var previousCli = Array.isArray(group.pairCliIds) ? group.pairCliIds : [null, null];
|
|
96
|
+
var previousOrigin = Array.isArray(group.pairOriginIds) ? group.pairOriginIds : [null, null];
|
|
97
|
+
var driver = pairFor(sessions.get(group.pair.driverId), previousCli[0], previousOrigin[0]);
|
|
98
|
+
var worker = pairFor(sessions.get(group.pair.workerId), previousCli[1], previousOrigin[1]);
|
|
99
|
+
return { cli: [driver.cli, worker.cli], origin: [driver.origin, worker.origin] };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function findSession(sessions, cliId, originId) {
|
|
103
|
+
var cli = value(cliId);
|
|
104
|
+
var origin = value(originId);
|
|
105
|
+
if (!cli && !origin) return null;
|
|
106
|
+
var found = null;
|
|
107
|
+
var ambiguous = false;
|
|
108
|
+
sessions.forEach(function (session) {
|
|
109
|
+
if (cli && session.cliSessionId !== cli) return;
|
|
110
|
+
if (origin && session.sessionOriginId !== origin) return;
|
|
111
|
+
if (found && found !== session) ambiguous = true;
|
|
112
|
+
else found = session;
|
|
113
|
+
});
|
|
114
|
+
return ambiguous ? null : found;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
findSession: findSession,
|
|
119
|
+
memberAnchors: memberAnchors,
|
|
120
|
+
pairAnchors: pairAnchors,
|
|
121
|
+
resolveVersionedMembers: resolveVersionedMembers,
|
|
122
|
+
resolveVersionedPair: resolveVersionedPair,
|
|
123
|
+
versionedAnchor: versionedAnchor,
|
|
124
|
+
versionedMemberAnchors: versionedMemberAnchors,
|
|
125
|
+
versionedPairAnchors: versionedPairAnchors,
|
|
126
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
var fs = require("fs");
|
|
2
|
+
var path = require("path");
|
|
3
|
+
var anchors = require("./session-split-group-anchors");
|
|
4
|
+
var roles = require("./session-split-group-roles");
|
|
5
|
+
|
|
6
|
+
var ANCHOR_KEYS = ["memberCliIds", "memberOriginIds", "pairCliIds", "pairOriginIds", "memberAnchors", "pairAnchors"];
|
|
7
|
+
|
|
8
|
+
function attachPersistence(opts) {
|
|
9
|
+
var sessions = opts.sessions;
|
|
10
|
+
var groupsFile = path.join(opts.sessionsDir, "split-groups.json");
|
|
11
|
+
var preserved = [];
|
|
12
|
+
|
|
13
|
+
function anchorPair(group) {
|
|
14
|
+
if (!group.pair) return;
|
|
15
|
+
var pair = anchors.pairAnchors(group, sessions);
|
|
16
|
+
if (!pair) return;
|
|
17
|
+
if (pair.version === 2) {
|
|
18
|
+
group.pairAnchors = pair;
|
|
19
|
+
delete group.pairCliIds; delete group.pairOriginIds;
|
|
20
|
+
} else {
|
|
21
|
+
group.pairCliIds = pair.cli; group.pairOriginIds = pair.origin;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function anchoredClone(group) {
|
|
26
|
+
var clone = Object.assign({}, group, { members: group.members.slice(),
|
|
27
|
+
pair: group.pair ? Object.assign({}, group.pair) : undefined });
|
|
28
|
+
if (clone.pair && Array.isArray(clone.pair.workerIds)) clone.pair.workerIds = clone.pair.workerIds.slice();
|
|
29
|
+
var member = anchors.memberAnchors(clone, sessions);
|
|
30
|
+
if (!member) throw new Error("Split group does not have durable session anchors");
|
|
31
|
+
if (member.version === 2) {
|
|
32
|
+
clone.memberAnchors = member;
|
|
33
|
+
delete clone.memberCliIds; delete clone.memberOriginIds;
|
|
34
|
+
} else {
|
|
35
|
+
clone.memberCliIds = member.cli; clone.memberOriginIds = member.origin;
|
|
36
|
+
}
|
|
37
|
+
anchorPair(clone);
|
|
38
|
+
return clone;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function save(groups) {
|
|
42
|
+
var saved = groups.map(anchoredClone);
|
|
43
|
+
var serialized = JSON.stringify(saved.concat(preserved), null, 2) + "\n";
|
|
44
|
+
if (typeof opts.persistGroups === "function") {
|
|
45
|
+
if (opts.persistGroups(groupsFile, serialized) === false) throw new Error("Split group persistence returned false");
|
|
46
|
+
} else {
|
|
47
|
+
var tmp = groupsFile + ".tmp." + process.pid;
|
|
48
|
+
fs.mkdirSync(opts.sessionsDir, { recursive: true });
|
|
49
|
+
fs.writeFileSync(tmp, serialized);
|
|
50
|
+
if (process.platform !== "win32") try { fs.chmodSync(tmp, 0o600); } catch (e) {}
|
|
51
|
+
fs.renameSync(tmp, groupsFile);
|
|
52
|
+
}
|
|
53
|
+
for (var i = 0; i < groups.length; i++) {
|
|
54
|
+
(function (live, stored) {
|
|
55
|
+
ANCHOR_KEYS.forEach(function (key) {
|
|
56
|
+
if (Object.prototype.hasOwnProperty.call(stored, key)) live[key] = stored[key];
|
|
57
|
+
else delete live[key];
|
|
58
|
+
});
|
|
59
|
+
})(groups[i], saved[i]);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveVersioned(group, normalized) {
|
|
64
|
+
if (typeof group.id !== "string" || !group.id) return false;
|
|
65
|
+
var members = anchors.resolveVersionedMembers(sessions, group.memberAnchors);
|
|
66
|
+
var pair = anchors.resolveVersionedPair(sessions, group.pairAnchors);
|
|
67
|
+
if (!members || !pair) return false;
|
|
68
|
+
var memberIds = members.map(function (session) { return session.localId; });
|
|
69
|
+
var workerIds = pair.workers.map(function (session) { return session.localId; });
|
|
70
|
+
if (memberIds.length !== group.members.length || workerIds.length !== normalized.workerIds.length) return false;
|
|
71
|
+
var nextPair = { version: 2, driverId: pair.driver.localId, workerIds: workerIds };
|
|
72
|
+
var nextRoles = roles.normalizePair(nextPair, memberIds);
|
|
73
|
+
if (!nextRoles.ok || nextRoles.kind !== "versioned") return false;
|
|
74
|
+
var changed = group.members.join(",") !== memberIds.join(",") ||
|
|
75
|
+
group.pair.driverId !== pair.driver.localId || group.pair.workerIds.join(",") !== workerIds.join(",");
|
|
76
|
+
group.members = memberIds;
|
|
77
|
+
group.pair = nextPair;
|
|
78
|
+
return { changed: changed };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveLegacy(group) {
|
|
82
|
+
var changed = false;
|
|
83
|
+
if (Array.isArray(group.memberCliIds) || Array.isArray(group.memberOriginIds)) {
|
|
84
|
+
var cli = Array.isArray(group.memberCliIds) ? group.memberCliIds : [null, null];
|
|
85
|
+
var origin = Array.isArray(group.memberOriginIds) ? group.memberOriginIds : [null, null];
|
|
86
|
+
var left = anchors.findSession(sessions, cli[0], origin[0]);
|
|
87
|
+
var right = anchors.findSession(sessions, cli[1], origin[1]);
|
|
88
|
+
if (!left || !right || left === right) return false;
|
|
89
|
+
if (group.members[0] !== left.localId || group.members[1] !== right.localId) {
|
|
90
|
+
group.members = [left.localId, right.localId]; changed = true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (group.pair && (Array.isArray(group.pairCliIds) || Array.isArray(group.pairOriginIds))) {
|
|
94
|
+
var pairCli = Array.isArray(group.pairCliIds) ? group.pairCliIds : [null, null];
|
|
95
|
+
var pairOrigin = Array.isArray(group.pairOriginIds) ? group.pairOriginIds : [null, null];
|
|
96
|
+
var driver = anchors.findSession(sessions, pairCli[0], pairOrigin[0]);
|
|
97
|
+
var worker = anchors.findSession(sessions, pairCli[1], pairOrigin[1]);
|
|
98
|
+
if (driver && worker && (group.pair.driverId !== driver.localId || group.pair.workerId !== worker.localId)) {
|
|
99
|
+
group.pair.driverId = driver.localId; group.pair.workerId = worker.localId; changed = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return roles.validateStoredGroup(group, sessions) ? { changed: changed } : false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function load() {
|
|
106
|
+
var loaded = [], rewrite = false, claimed = {};
|
|
107
|
+
preserved = [];
|
|
108
|
+
try { loaded = JSON.parse(fs.readFileSync(groupsFile, "utf8")); if (!Array.isArray(loaded)) loaded = []; }
|
|
109
|
+
catch (error) { if (error.code !== "ENOENT") rewrite = true; }
|
|
110
|
+
var groups = loaded.filter(function (group) {
|
|
111
|
+
var normalized = group && roles.normalizePair(group.pair, group.members);
|
|
112
|
+
if (!normalized || !normalized.ok) {
|
|
113
|
+
if (group && roles.isUnsupportedPair(group.pair)) preserved.push(group);
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
var versioned = !!(normalized && normalized.ok && normalized.kind === "versioned");
|
|
117
|
+
if (versioned && !opts.versionedEnabled) { preserved.push(group); return false; }
|
|
118
|
+
var resolved = versioned ? resolveVersioned(group, normalized) : resolveLegacy(group);
|
|
119
|
+
if (!resolved) {
|
|
120
|
+
if (versioned || group && roles.isUnsupportedPair(group.pair)) preserved.push(group);
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
if (resolved.changed) rewrite = true;
|
|
124
|
+
for (var i = 0; i < group.members.length; i++) if (claimed[group.members[i]]) return false;
|
|
125
|
+
for (var j = 0; j < group.members.length; j++) claimed[group.members[j]] = true;
|
|
126
|
+
return true;
|
|
127
|
+
});
|
|
128
|
+
if (groups.length + preserved.length !== loaded.length) rewrite = true;
|
|
129
|
+
if (rewrite) save(groups);
|
|
130
|
+
return groups;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { load: load, save: save };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = { attachPersistence: attachPersistence };
|