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.
Files changed (84) hide show
  1. package/lib/driver-continuation-access.js +54 -0
  2. package/lib/driver-continuation-lease.js +52 -0
  3. package/lib/driver-continuation-pair-transfer.js +86 -0
  4. package/lib/driver-continuation-pair.js +86 -0
  5. package/lib/driver-continuation-record.js +40 -0
  6. package/lib/driver-continuation-startup.js +68 -0
  7. package/lib/driver-continuation-transaction.js +61 -0
  8. package/lib/driver-continuation-trigger.js +110 -0
  9. package/lib/multi-worker-feature.js +11 -0
  10. package/lib/project-connection.js +3 -1
  11. package/lib/project-driver-continuation.js +499 -0
  12. package/lib/project-message-delivery.js +20 -0
  13. package/lib/project-pair-autonomous-stop.js +14 -8
  14. package/lib/project-pair-close-transaction.js +59 -0
  15. package/lib/project-pair-global-stop.js +18 -0
  16. package/lib/project-pair-lifecycle-status.js +34 -0
  17. package/lib/project-pair-lifecycle.js +117 -135
  18. package/lib/project-pair-message.js +1 -1
  19. package/lib/project-pair-owned-stop.js +7 -4
  20. package/lib/project-pair-replacement-state.js +5 -1
  21. package/lib/project-pair-result-capture.js +116 -0
  22. package/lib/project-pair-result-delivery.js +140 -0
  23. package/lib/project-pair-result-outbox.js +400 -0
  24. package/lib/project-pair-result-pipeline.js +16 -0
  25. package/lib/project-pair-result-recovery.js +229 -0
  26. package/lib/project-pair-structural-close.js +20 -0
  27. package/lib/project-pair-task-control.js +45 -18
  28. package/lib/project-pair-worker-close.js +68 -0
  29. package/lib/project-session-handoff.js +48 -55
  30. package/lib/project-session-pair-prompt.js +26 -0
  31. package/lib/project-session-pair.js +87 -106
  32. package/lib/project-sessions.js +4 -0
  33. package/lib/project-user-message.js +21 -0
  34. package/lib/project-worker-creation-proposal.js +176 -0
  35. package/lib/project-worker-permission.js +7 -8
  36. package/lib/project-worker-proposal-runner.js +45 -0
  37. package/lib/project-worker-proposal.js +79 -115
  38. package/lib/project.js +87 -3
  39. package/lib/public/app.js +1 -0
  40. package/lib/public/css/driver-continuation.css +56 -0
  41. package/lib/public/css/messages.css +12 -0
  42. package/lib/public/css/mobile-nav.css +29 -14
  43. package/lib/public/css/overlays.css +54 -25
  44. package/lib/public/css/pane.css +66 -12
  45. package/lib/public/css/sidebar.css +62 -7
  46. package/lib/public/index.html +3 -3
  47. package/lib/public/modules/app-connection.js +165 -74
  48. package/lib/public/modules/app-messages.js +31 -3
  49. package/lib/public/modules/driver-continuation-state.js +76 -0
  50. package/lib/public/modules/driver-continuation.js +260 -0
  51. package/lib/public/modules/message-delivery-ui.js +63 -0
  52. package/lib/public/modules/message-delivery.js +163 -6
  53. package/lib/public/modules/pair-result-status.js +87 -0
  54. package/lib/public/modules/permission-control.js +10 -5
  55. package/lib/public/modules/project-activation.js +10 -7
  56. package/lib/public/modules/session-creation-cta.js +46 -0
  57. package/lib/public/modules/sidebar-mobile.js +42 -30
  58. package/lib/public/modules/sidebar-sessions.js +77 -52
  59. package/lib/public/modules/split-group-helpers.js +74 -2
  60. package/lib/public/modules/split-pair-ui.js +28 -6
  61. package/lib/public/modules/split-pane-reconciler.js +28 -0
  62. package/lib/public/modules/split-pane-renderer.js +107 -0
  63. package/lib/public/modules/split-view.js +58 -188
  64. package/lib/public/modules/websocket-lifecycle.js +178 -0
  65. package/lib/public/modules/websocket-watchdog.js +126 -0
  66. package/lib/public/modules/worker-pane-lock.js +13 -3
  67. package/lib/public/style.css +1 -0
  68. package/lib/sdk-bridge.js +77 -0
  69. package/lib/server.js +1 -0
  70. package/lib/session-handoff-discovery.js +443 -0
  71. package/lib/session-handoff-mcp-server.js +32 -4
  72. package/lib/session-pair-factory.js +66 -0
  73. package/lib/session-pair-mcp-server.js +10 -5
  74. package/lib/session-pair-prompts.js +46 -25
  75. package/lib/session-pair-target.js +42 -0
  76. package/lib/session-pair-turn-control.js +30 -9
  77. package/lib/session-split-group-anchors.js +126 -0
  78. package/lib/session-split-group-persistence.js +136 -0
  79. package/lib/session-split-group-roles.js +164 -0
  80. package/lib/session-split-group-v2-store.js +146 -0
  81. package/lib/session-split-groups.js +293 -133
  82. package/lib/sessions.js +89 -22
  83. package/lib/ws-schema.js +8 -0
  84. package/package.json +1 -1
@@ -0,0 +1,164 @@
1
+ // Shared role and target identity rules for Split Groups.
2
+ //
3
+ // Version 1 is the persisted two-session pair shape. Version 2 describes one
4
+ // Driver and up to two Workers, but callers must explicitly opt into it until
5
+ // all production consumers can address more than one Worker.
6
+ // Remaining direct pair.workerId reads are deliberate compatibility branches:
7
+ // session-split-groups owns legacy-only create and Driver-transfer transactions,
8
+ // while session-split-group-anchors and persistence resolve legacy records.
9
+ // Production activation must still migrate the client hierarchy, pane lock, and
10
+ // split-group helper projections before the server gate is enabled.
11
+
12
+ var MAX_WORKERS = 2;
13
+
14
+ function isInteger(value) {
15
+ return Number.isInteger(value);
16
+ }
17
+
18
+ function containsOnce(values, value) {
19
+ var count = 0;
20
+ for (var i = 0; i < values.length; i++) if (values[i] === value) count++;
21
+ return count === 1;
22
+ }
23
+
24
+ function validateMembers(members) {
25
+ if (!Array.isArray(members) || (members.length !== 2 && members.length !== 3)) {
26
+ return { ok: false, error: "A split group requires exactly two sessions until multi-Worker consumers are enabled" };
27
+ }
28
+ for (var i = 0; i < members.length; i++) {
29
+ if (!isInteger(members[i])) return { ok: false, error: "Split group members must be distinct integer session ids" };
30
+ if (!containsOnce(members, members[i])) return { ok: false, error: "Split group members must be distinct integer session ids" };
31
+ }
32
+ return { ok: true };
33
+ }
34
+
35
+ function validateRoleIds(driverId, workerIds, members) {
36
+ if (!isInteger(driverId)) return { ok: false, error: "The Driver role must reference an integer session id" };
37
+ if (!Array.isArray(workerIds) || workerIds.length < 1 || workerIds.length > MAX_WORKERS) {
38
+ return { ok: false, error: "A configured role requires one to two Workers" };
39
+ }
40
+ if (!containsOnce(members, driverId)) return { ok: false, error: "The Driver role must reference exactly one split group member" };
41
+ var seen = [driverId];
42
+ for (var i = 0; i < workerIds.length; i++) {
43
+ if (!isInteger(workerIds[i]) || seen.indexOf(workerIds[i]) !== -1 || !containsOnce(members, workerIds[i])) {
44
+ return { ok: false, error: "Worker roles must reference distinct split group members" };
45
+ }
46
+ seen.push(workerIds[i]);
47
+ }
48
+ if (seen.length !== members.length) return { ok: false, error: "Every split group member must have exactly one role" };
49
+ return { ok: true };
50
+ }
51
+
52
+ function normalizePair(pair, members) {
53
+ var memberCheck = validateMembers(members);
54
+ if (!memberCheck.ok) return memberCheck;
55
+ if (pair == null) {
56
+ if (members.length !== 2) return { ok: false, error: "An ad-hoc split group requires exactly two sessions" };
57
+ return { ok: true, kind: "adhoc", version: null, driverId: null, workerIds: [] };
58
+ }
59
+ if (!pair || typeof pair !== "object" || Array.isArray(pair)) return { ok: false, error: "Split group roles must be an object" };
60
+
61
+ var hasVersion = Object.prototype.hasOwnProperty.call(pair, "version");
62
+ var hasLegacyWorker = Object.prototype.hasOwnProperty.call(pair, "workerId");
63
+ var hasWorkerIds = Object.prototype.hasOwnProperty.call(pair, "workerIds");
64
+ if (hasVersion && pair.version !== 2) return { ok: false, error: "Unknown split group role version" };
65
+ if (hasVersion && hasLegacyWorker) return { ok: false, error: "Versioned roles cannot also define legacy workerId" };
66
+ if (hasVersion && !hasWorkerIds) return { ok: false, error: "Version 2 roles require workerIds" };
67
+ if (!hasVersion && hasWorkerIds) return { ok: false, error: "workerIds requires version 2 roles" };
68
+ if (!hasVersion && !hasLegacyWorker) return { ok: false, error: "Legacy roles require workerId" };
69
+
70
+ var workerIds = hasVersion ? pair.workerIds : [pair.workerId];
71
+ var checked = validateRoleIds(pair.driverId, workerIds, members);
72
+ if (!checked.ok) return checked;
73
+ return {
74
+ ok: true,
75
+ kind: hasVersion ? "versioned" : "legacy",
76
+ version: hasVersion ? 2 : null,
77
+ driverId: pair.driverId,
78
+ workerIds: workerIds.slice(),
79
+ };
80
+ }
81
+
82
+ function validateStorePair(pair, members) {
83
+ var normalized = normalizePair(pair, members);
84
+ if (!normalized.ok) return normalized;
85
+ if (normalized.kind === "versioned") {
86
+ return { ok: false, error: "Version 2 split group roles are reserved until multi-Worker consumers are migrated" };
87
+ }
88
+ if (normalized.kind === "adhoc" && members.length !== 2) {
89
+ return { ok: false, error: "An ad-hoc split group requires exactly two sessions" };
90
+ }
91
+ return normalized;
92
+ }
93
+
94
+ function isUnsupportedPair(pair) {
95
+ return !!(pair && typeof pair === "object" && Object.prototype.hasOwnProperty.call(pair, "version"));
96
+ }
97
+
98
+ function validateStoredGroup(group, sessions) {
99
+ if (!group || typeof group.id !== "string" || !Array.isArray(group.members) || group.members.length !== 2) return false;
100
+ if (!sessions.has(group.members[0]) || !sessions.has(group.members[1])) return false;
101
+ return validateStorePair(group.pair, group.members).ok;
102
+ }
103
+
104
+ function resolveWorkerTarget(role, requestedWorkerId) {
105
+ if (!role || !role.ok || role.kind === "adhoc") return { ok: false, error: "The split group has no configured Workers" };
106
+ if (requestedWorkerId != null) {
107
+ if (!isInteger(requestedWorkerId) || role.workerIds.indexOf(requestedWorkerId) === -1) {
108
+ return { ok: false, error: "workerId must identify an exact configured Worker" };
109
+ }
110
+ return { ok: true, workerId: requestedWorkerId };
111
+ }
112
+ if (role.workerIds.length !== 1) return { ok: false, error: "workerId is required when more than one Worker is configured" };
113
+ return { ok: true, workerId: role.workerIds[0] };
114
+ }
115
+
116
+ function resolveLiveRoles(sm, group) {
117
+ var normalized = normalizePair(group && group.pair, group && group.members);
118
+ if (!normalized.ok || normalized.kind === "adhoc") return null;
119
+ var driver = sm.sessions.get(normalized.driverId);
120
+ if (!driver || sm.sessions.get(driver.localId) !== driver || driver.destroying) return null;
121
+ var workers = [];
122
+ for (var i = 0; i < normalized.workerIds.length; i++) {
123
+ var worker = sm.sessions.get(normalized.workerIds[i]);
124
+ if (!worker || sm.sessions.get(worker.localId) !== worker || worker.destroying) return null;
125
+ if ((worker.ownerId || null) !== (driver.ownerId || null)) return null;
126
+ workers.push(worker);
127
+ }
128
+ return { group: group, role: normalized, driver: driver, workers: workers };
129
+ }
130
+
131
+ function matchesLiveTarget(sm, group, caller, worker) {
132
+ if (!group || !caller || !worker) return false;
133
+ if (sm.sessions.get(caller.localId) !== caller || sm.sessions.get(worker.localId) !== worker) return false;
134
+ if ((caller.ownerId || null) !== (worker.ownerId || null)) return false;
135
+ var normalized = normalizePair(group.pair, group.members);
136
+ if (!normalized.ok) return false;
137
+ if (normalized.kind === "adhoc") {
138
+ return group.members.indexOf(caller.localId) !== -1 && group.members.indexOf(worker.localId) !== -1 && caller !== worker;
139
+ }
140
+ var roles = resolveLiveRoles(sm, group);
141
+ return !!(roles && roles.driver === caller && roles.workers.indexOf(worker) !== -1);
142
+ }
143
+
144
+ function taskGeneration(worker) {
145
+ return worker && (worker._pairGeneration || worker.sessionProvenance && worker.sessionProvenance.generation) || null;
146
+ }
147
+
148
+ function tokenMatchesWorker(worker, token) {
149
+ return !!(worker && token && token.workerSessionId === worker.localId && token.generation === taskGeneration(worker));
150
+ }
151
+
152
+ module.exports = {
153
+ MAX_WORKERS: MAX_WORKERS,
154
+ normalizePair: normalizePair,
155
+ matchesLiveTarget: matchesLiveTarget,
156
+ resolveLiveRoles: resolveLiveRoles,
157
+ resolveWorkerTarget: resolveWorkerTarget,
158
+ tokenMatchesWorker: tokenMatchesWorker,
159
+ validateMembers: validateMembers,
160
+ validateRoleIds: validateRoleIds,
161
+ validateStoredGroup: validateStoredGroup,
162
+ validateStorePair: validateStorePair,
163
+ isUnsupportedPair: isUnsupportedPair,
164
+ };
@@ -0,0 +1,146 @@
1
+ var multiWorkerFeature = require("./multi-worker-feature");
2
+ var splitRoles = require("./session-split-group-roles");
3
+
4
+ function sameOwner(session, ownerId) {
5
+ return !!session && (session.ownerId || null) === (ownerId || null);
6
+ }
7
+
8
+ function generationOf(session) {
9
+ return session && (session._pairGeneration ||
10
+ session.sessionProvenance && session.sessionProvenance.generation) || null;
11
+ }
12
+
13
+ function clone(value) { return JSON.parse(JSON.stringify(value)); }
14
+
15
+ function replace(target, source) {
16
+ var keys = Object.keys(target);
17
+ for (var i = 0; i < keys.length; i++) delete target[keys[i]];
18
+ Object.assign(target, clone(source));
19
+ }
20
+
21
+ function attachVersionedMutations(ctx) {
22
+ function authorized(feature) {
23
+ return ctx.enabled && multiWorkerFeature.isEnabled(feature);
24
+ }
25
+
26
+ function rejectCapability(capability) {
27
+ if (authorized(capability)) return null;
28
+ return { ok: false, error: "Version 2 split groups require the server multi-Worker feature" };
29
+ }
30
+
31
+ function exactGroup(ownerId, msg, targetRequired) {
32
+ var group = ctx.groups().find(function (item) { return item.id === (msg && msg.groupId); });
33
+ if (!group) return { error: "Split group not found" };
34
+ if (msg.expectedGroup && msg.expectedGroup !== group) return { error: "The exact split group changed" };
35
+ if ((group.ownerId || null) !== (ownerId || null)) return { error: "Split group owner changed" };
36
+ var normalized = splitRoles.normalizePair(group.pair, group.members);
37
+ if (!normalized.ok || normalized.kind === "adhoc") return { error: "Split group roles changed" };
38
+ if (normalized.driverId !== msg.driverId) return { error: "The configured Driver changed" };
39
+ if (Array.isArray(msg.expectedWorkerIds) &&
40
+ normalized.workerIds.join(",") !== msg.expectedWorkerIds.join(",")) {
41
+ return { error: "The configured Worker membership changed" };
42
+ }
43
+ var driver = ctx.sessions.get(msg.driverId);
44
+ if (!sameOwner(driver, ownerId) || ctx.sessions.get(driver.localId) !== driver ||
45
+ (msg.driverOriginId || null) !== (driver.sessionOriginId || null)) {
46
+ return { error: "The exact Driver identity changed" };
47
+ }
48
+ var target = null;
49
+ if (targetRequired) {
50
+ if (normalized.workerIds.indexOf(msg.workerId) === -1) return { error: "The selected Worker changed" };
51
+ target = ctx.sessions.get(msg.workerId);
52
+ if (!sameOwner(target, ownerId) || ctx.sessions.get(target.localId) !== target ||
53
+ (msg.workerOriginId || null) !== (target.sessionOriginId || null) ||
54
+ msg.generation !== generationOf(target)) {
55
+ return { error: "The exact Worker identity or generation changed" };
56
+ }
57
+ }
58
+ return { group: group, normalized: normalized, driver: driver, target: target };
59
+ }
60
+
61
+ function newWorker(ownerId, msg) {
62
+ var worker = ctx.sessions.get(msg.newWorkerId);
63
+ if (!sameOwner(worker, ownerId) || ctx.sessions.get(worker.localId) !== worker ||
64
+ (msg.newWorkerOriginId || null) !== (worker.sessionOriginId || null)) {
65
+ return { error: "The exact replacement Worker identity changed" };
66
+ }
67
+ if (ctx.membershipGroup(worker.localId)) return { error: "The Worker already belongs to a split group" };
68
+ return { worker: worker };
69
+ }
70
+
71
+ function commit(group, nextMembers, nextPair, hooks) {
72
+ var before = clone(group);
73
+ group.members = nextMembers;
74
+ group.pair = nextPair;
75
+ try { ctx.save(); }
76
+ catch (error) {
77
+ replace(group, before);
78
+ return { ok: false, error: error.message || String(error) };
79
+ }
80
+ var committed = clone(group);
81
+ if (hooks && typeof hooks.afterPersist === "function") {
82
+ replace(group, before);
83
+ var accepted;
84
+ try { accepted = hooks.afterPersist(); }
85
+ catch (error) { accepted = { ok: false, error: error.message || String(error) }; }
86
+ if (!accepted || accepted.ok === false) {
87
+ try { ctx.save(); }
88
+ catch (rollbackError) {
89
+ return { ok: false, error: (accepted && accepted.error || "close transaction rejected") +
90
+ "; persistence rollback failed: " + (rollbackError.message || String(rollbackError)) };
91
+ }
92
+ return { ok: false, error: accepted && accepted.error || "close transaction rejected" };
93
+ }
94
+ replace(group, committed);
95
+ }
96
+ ctx.changed(group);
97
+ return { ok: true, group: group };
98
+ }
99
+
100
+ function addWorker(ownerId, msg, capability) {
101
+ var denied = rejectCapability(capability);
102
+ if (denied) return denied;
103
+ var exact = exactGroup(ownerId, msg || {}, false);
104
+ if (exact.error) return { ok: false, error: exact.error };
105
+ if (exact.normalized.workerIds.length >= splitRoles.MAX_WORKERS) {
106
+ return { ok: false, error: "A Driver can have at most two Workers" };
107
+ }
108
+ var added = newWorker(ownerId, msg);
109
+ if (added.error) return { ok: false, error: added.error };
110
+ var workerIds = exact.normalized.workerIds.concat([added.worker.localId]);
111
+ return commit(exact.group, [exact.driver.localId].concat(workerIds), {
112
+ version: 2, driverId: exact.driver.localId, workerIds: workerIds,
113
+ });
114
+ }
115
+
116
+ function replaceWorker(ownerId, msg, capability) {
117
+ var denied = rejectCapability(capability);
118
+ if (denied) return denied;
119
+ var exact = exactGroup(ownerId, msg || {}, true);
120
+ if (exact.error) return { ok: false, error: exact.error };
121
+ var added = newWorker(ownerId, msg);
122
+ if (added.error) return { ok: false, error: added.error };
123
+ var workerIds = exact.normalized.workerIds.slice();
124
+ workerIds[workerIds.indexOf(exact.target.localId)] = added.worker.localId;
125
+ var pair = exact.normalized.kind === "legacy"
126
+ ? { driverId: exact.driver.localId, workerId: added.worker.localId }
127
+ : { version: 2, driverId: exact.driver.localId, workerIds: workerIds };
128
+ return commit(exact.group, [exact.driver.localId].concat(workerIds), pair);
129
+ }
130
+
131
+ function removeWorker(ownerId, msg, capability, hooks) {
132
+ var denied = rejectCapability(capability);
133
+ if (denied) return denied;
134
+ var exact = exactGroup(ownerId, msg || {}, true);
135
+ if (exact.error) return { ok: false, error: exact.error };
136
+ var workerIds = exact.normalized.workerIds.filter(function (id) { return id !== exact.target.localId; });
137
+ if (workerIds.length === 0) return ctx.remove(exact.group, hooks);
138
+ return commit(exact.group, [exact.driver.localId].concat(workerIds), {
139
+ version: 2, driverId: exact.driver.localId, workerIds: workerIds,
140
+ }, hooks);
141
+ }
142
+
143
+ return { addWorker: addWorker, replaceWorker: replaceWorker, removeWorker: removeWorker };
144
+ }
145
+
146
+ module.exports = { attachVersionedMutations: attachVersionedMutations, generationOf: generationOf };