clay-server 4.2.0 → 4.3.0-beta.2

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 (40) 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 +82 -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/project-connection.js +1 -1
  10. package/lib/project-driver-continuation.js +499 -0
  11. package/lib/project-message-delivery.js +20 -0
  12. package/lib/project-pair-lifecycle.js +73 -73
  13. package/lib/project-pair-replacement-state.js +5 -1
  14. package/lib/project-session-handoff.js +48 -55
  15. package/lib/project-user-message.js +21 -0
  16. package/lib/project-worker-proposal.js +1 -1
  17. package/lib/project.js +49 -2
  18. package/lib/public/css/driver-continuation.css +56 -0
  19. package/lib/public/css/overlays.css +44 -16
  20. package/lib/public/css/pane.css +9 -5
  21. package/lib/public/index.html +3 -3
  22. package/lib/public/modules/app-connection.js +169 -51
  23. package/lib/public/modules/app-messages.js +19 -3
  24. package/lib/public/modules/driver-continuation-state.js +76 -0
  25. package/lib/public/modules/driver-continuation.js +260 -0
  26. package/lib/public/modules/message-delivery-ui.js +63 -0
  27. package/lib/public/modules/message-delivery.js +163 -6
  28. package/lib/public/modules/permission-control.js +8 -3
  29. package/lib/public/modules/websocket-lifecycle.js +178 -0
  30. package/lib/public/style.css +1 -0
  31. package/lib/sdk-bridge.js +77 -0
  32. package/lib/session-handoff-discovery.js +443 -0
  33. package/lib/session-handoff-mcp-server.js +32 -4
  34. package/lib/session-pair-prompts.js +5 -0
  35. package/lib/session-pair-turn-control.js +19 -3
  36. package/lib/session-split-group-anchors.js +44 -0
  37. package/lib/session-split-groups.js +209 -59
  38. package/lib/sessions.js +21 -3
  39. package/lib/ws-schema.js +6 -0
  40. package/package.json +1 -1
@@ -0,0 +1,178 @@
1
+ // websocket-lifecycle.js - bounded connection attempt and retry policy
2
+
3
+ var DEFAULTS = {
4
+ handshakeTimeoutMs: 12000,
5
+ stableConnectionMs: 15000,
6
+ retryMinMs: 1000,
7
+ retryMaxMs: 15000,
8
+ jitterRatio: 0.2,
9
+ authTimeoutMs: 4000
10
+ };
11
+
12
+ function createWebSocketLifecycle(options) {
13
+ var config = Object.assign({}, DEFAULTS, options || {});
14
+ var epoch = 0;
15
+ var retryAttempt = 0;
16
+ var retryTimer = null;
17
+ var handshakeTimer = null;
18
+ var stableTimer = null;
19
+ var authTimer = null;
20
+ var authInFlight = false;
21
+ var authCancel = null;
22
+ var authGeneration = 0;
23
+ var stableEpoch = 0;
24
+ var cancelled = false;
25
+ var offline = false;
26
+
27
+ function clearTimer(timer) {
28
+ if (timer) clearTimeout(timer);
29
+ return null;
30
+ }
31
+
32
+ function clearTimers() {
33
+ retryTimer = clearTimer(retryTimer);
34
+ handshakeTimer = clearTimer(handshakeTimer);
35
+ stableTimer = clearTimer(stableTimer);
36
+ authTimer = clearTimer(authTimer);
37
+ stableEpoch = 0;
38
+ if (authCancel) authCancel();
39
+ authCancel = null;
40
+ authInFlight = false;
41
+ }
42
+
43
+ function current(value) {
44
+ return value === epoch && !cancelled;
45
+ }
46
+
47
+ function canProceed(value) {
48
+ return current(value) && !offline;
49
+ }
50
+
51
+ function beginAttempt() {
52
+ clearTimers();
53
+ cancelled = false;
54
+ epoch += 1;
55
+ var attemptEpoch = epoch;
56
+ handshakeTimer = setTimeout(function () {
57
+ handshakeTimer = null;
58
+ if (current(attemptEpoch) && config.onHandshakeTimeout) config.onHandshakeTimeout(attemptEpoch);
59
+ }, config.handshakeTimeoutMs);
60
+ return attemptEpoch;
61
+ }
62
+
63
+ function delayForAttempt() {
64
+ var base = Math.min(config.retryMaxMs, config.retryMinMs * Math.pow(1.5, retryAttempt));
65
+ retryAttempt += 1;
66
+ var jitter = base * config.jitterRatio;
67
+ var random = config.random || Math.random;
68
+ return Math.min(config.retryMaxMs, Math.max(0, Math.round(base - jitter + random() * jitter * 2)));
69
+ }
70
+
71
+ function schedule(reason, callback) {
72
+ if (cancelled || offline || retryTimer) return null;
73
+ var scheduledEpoch = epoch;
74
+ var delay = delayForAttempt();
75
+ retryTimer = setTimeout(function () {
76
+ retryTimer = null;
77
+ if (!canProceed(scheduledEpoch)) return;
78
+ callback(scheduledEpoch, reason, delay);
79
+ }, delay);
80
+ return { epoch: scheduledEpoch, delay: delay };
81
+ }
82
+
83
+ function clearRetry() {
84
+ retryTimer = clearTimer(retryTimer);
85
+ }
86
+
87
+ function beginAuth(authEpoch, onTimeout, onCancel) {
88
+ if (!canProceed(authEpoch) || authInFlight) return null;
89
+ authInFlight = true;
90
+ authGeneration += 1;
91
+ var authToken = { epoch: authEpoch, generation: authGeneration };
92
+ authCancel = onCancel || null;
93
+ authTimer = setTimeout(function () {
94
+ authTimer = null;
95
+ authInFlight = false;
96
+ authCancel = null;
97
+ if (canProceed(authEpoch) && onTimeout) onTimeout(authEpoch);
98
+ }, config.authTimeoutMs);
99
+ return authToken;
100
+ }
101
+
102
+ function finishAuth(authEpoch, authToken) {
103
+ if (!authInFlight || !current(authEpoch) || !authToken || authToken.epoch !== authEpoch || authToken.generation !== authGeneration) return false;
104
+ authInFlight = false;
105
+ authCancel = null;
106
+ authTimer = clearTimer(authTimer);
107
+ return true;
108
+ }
109
+
110
+ function markOpen(attemptEpoch) {
111
+ if (!current(attemptEpoch)) return false;
112
+ handshakeTimer = clearTimer(handshakeTimer);
113
+ return true;
114
+ }
115
+
116
+ function markLive(attemptEpoch) {
117
+ if (!current(attemptEpoch)) return false;
118
+ handshakeTimer = clearTimer(handshakeTimer);
119
+ if (stableEpoch === attemptEpoch) return true;
120
+ stableEpoch = attemptEpoch;
121
+ stableTimer = clearTimer(stableTimer);
122
+ stableTimer = setTimeout(function () {
123
+ stableTimer = null;
124
+ if (current(attemptEpoch)) retryAttempt = 0;
125
+ }, config.stableConnectionMs);
126
+ return true;
127
+ }
128
+
129
+ function invalidate(attemptEpoch) {
130
+ if (attemptEpoch !== epoch) return false;
131
+ epoch += 1;
132
+ clearTimers();
133
+ return true;
134
+ }
135
+
136
+ function cancel() {
137
+ cancelled = true;
138
+ epoch += 1;
139
+ clearTimers();
140
+ }
141
+
142
+ function setOffline(value) {
143
+ var nextOffline = value === true;
144
+ if (nextOffline === offline) return offline;
145
+ offline = nextOffline;
146
+ if (offline) {
147
+ epoch += 1;
148
+ clearTimers();
149
+ }
150
+ return offline;
151
+ }
152
+
153
+ function wake(callback) {
154
+ offline = false;
155
+ if (!cancelled && callback) callback();
156
+ }
157
+
158
+ return {
159
+ beginAttempt: beginAttempt,
160
+ beginAuth: beginAuth,
161
+ cancel: cancel,
162
+ canProceed: canProceed,
163
+ clearRetry: clearRetry,
164
+ current: current,
165
+ finishAuth: finishAuth,
166
+ invalidate: invalidate,
167
+ markOpen: markOpen,
168
+ markLive: markLive,
169
+ schedule: schedule,
170
+ setOffline: setOffline,
171
+ wake: wake,
172
+ getRetryAttempt: function () { return retryAttempt; },
173
+ getEpoch: function () { return epoch; },
174
+ isOffline: function () { return offline; }
175
+ };
176
+ }
177
+
178
+ export { createWebSocketLifecycle };
@@ -26,6 +26,7 @@
26
26
  @import url("css/loop.css");
27
27
  @import url("css/scheduler.css");
28
28
  @import url("css/project-logs.css");
29
+ @import url("css/driver-continuation.css");
29
30
  @import url("css/scheduled-tasks.css");
30
31
  @import url("css/scheduler-modal.css");
31
32
  @import url("css/home-hub.css");
package/lib/sdk-bridge.js CHANGED
@@ -603,6 +603,10 @@ function createSDKBridge(opts) {
603
603
  return { behavior: "allow", updatedInput: input };
604
604
  }
605
605
 
606
+ if (toolName === "propose_driver_continuation" || toolName === "mcp__clay-continuation__propose_driver_continuation") {
607
+ return { behavior: "allow", updatedInput: input };
608
+ }
609
+
606
610
  var safeCapsuleTools = {
607
611
  clay_tool_list: true,
608
612
  clay_tool_snapshot: true,
@@ -1004,6 +1008,26 @@ function createSDKBridge(opts) {
1004
1008
  mode: session.permissionMode || sm.currentPermissionMode || "default", effectivePermissionMode: message.permissionMode,
1005
1009
  permissionCapabilities: session.permissionCapabilities || { auto: false, mcpOverride: false },
1006
1010
  mcpPermissionModeOverrides: session.mcpPermissionModeOverrides || {} });
1011
+ if (typeof sm.broadcastSessionList === "function") sm.broadcastSessionList();
1012
+ return true;
1013
+ }
1014
+
1015
+ function settleContinuationStartup(session, queryInstance, queryGeneration, accepted, reason) {
1016
+ var probe = session._continuationStartupProbe;
1017
+ if (!probe || probe.queryInstance !== queryInstance || probe.queryGeneration !== queryGeneration) return false;
1018
+ if (!ownsQueryRuntime(session, queryInstance, queryGeneration)) return false;
1019
+ delete session._continuationStartupProbe;
1020
+ clearTimeout(probe.timer);
1021
+ if (probe.timedOut) {
1022
+ if (typeof probe.onLateStartup === "function") probe.onLateStartup({
1023
+ accepted: accepted === true,
1024
+ queryAlive: session.queryInstance === queryInstance,
1025
+ queryGeneration: queryGeneration,
1026
+ reason: reason || null,
1027
+ });
1028
+ return true;
1029
+ }
1030
+ probe.resolve({ accepted: accepted === true, reason: reason || null });
1007
1031
  return true;
1008
1032
  }
1009
1033
 
@@ -1054,7 +1078,16 @@ function createSDKBridge(opts) {
1054
1078
  }
1055
1079
  continue;
1056
1080
  }
1081
+ var continuationIdentity = msg && (msg.yokeType === "init" || msg.yokeType === "session_started");
1082
+ if (continuationIdentity && !ownsQueryRuntime(session, myQueryInstance, myQueryGeneration)) {
1083
+ continue;
1084
+ }
1057
1085
  processSDKMessage(session, msg);
1086
+ if (continuationIdentity) {
1087
+ settleContinuationStartup(session, myQueryInstance, myQueryGeneration, true, null);
1088
+ } else if (msg && msg.yokeType === "error") {
1089
+ settleContinuationStartup(session, myQueryInstance, myQueryGeneration, false, "The successor reported an error before initialization.");
1090
+ }
1058
1091
  // SDK init/status events are the only authority for runtime-effective
1059
1092
  // permission state. Setter acknowledgement deliberately does not
1060
1093
  // change this field.
@@ -1075,6 +1108,7 @@ function createSDKBridge(opts) {
1075
1108
  // so the session is still marked as processing. Send interrupted feedback.
1076
1109
  console.log("[sdk-bridge] processQueryStream ended: isProcessing=" + session.isProcessing + " taskStopRequested=" + session.taskStopRequested);
1077
1110
  var stillOwnsRuntime = session.queryInstance === myQueryInstance;
1111
+ settleContinuationStartup(session, myQueryInstance, myQueryGeneration, false, "The successor query ended before initialization.");
1078
1112
  if (session.isProcessing && session.taskStopRequested && stillOwnsRuntime) {
1079
1113
  session._lastTurnInterrupted = true;
1080
1114
  session.isProcessing = false;
@@ -1108,6 +1142,7 @@ function createSDKBridge(opts) {
1108
1142
  sm.broadcastSessionList();
1109
1143
  }
1110
1144
  } catch (err) {
1145
+ settleContinuationStartup(session, myQueryInstance, myQueryGeneration, false, err.message || "The successor query failed before initialization.");
1111
1146
  if (session.isProcessing && session.queryInstance === myQueryInstance) {
1112
1147
  session.isProcessing = false;
1113
1148
  session._awaitingTurnResult = false;
@@ -1202,6 +1237,7 @@ function createSDKBridge(opts) {
1202
1237
  sm.broadcastSessionList();
1203
1238
  }
1204
1239
  } finally {
1240
+ settleContinuationStartup(session, myQueryInstance, myQueryGeneration, false, "The successor query closed before initialization.");
1205
1241
  // Close the SDK query to terminate the underlying claude child process.
1206
1242
  // Without this, the process stays alive indefinitely (single-user mode).
1207
1243
  // Only clean up if the session still references OUR resources.
@@ -1398,6 +1434,46 @@ function createSDKBridge(opts) {
1398
1434
  }
1399
1435
  }
1400
1436
 
1437
+ // Continuation startup needs stronger evidence than a resolved startQuery
1438
+ // promise: createQuery and initial delivery failures are intentionally
1439
+ // contained inside startQuery. Return success only when the initial message
1440
+ // was accepted and that exact query emitted its provider initialization event.
1441
+ async function startQueryWithAcceptance(session, text, images, linuxUser, beforePush, onLateStartup) {
1442
+ var acceptedHandle = null;
1443
+ var acceptedGeneration = null;
1444
+ var resolveStartup;
1445
+ var startup = new Promise(function (resolve) { resolveStartup = resolve; });
1446
+ var result = await startQuery(session, text, images, linuxUser, beforePush, function () {
1447
+ acceptedHandle = session.queryInstance || null;
1448
+ acceptedGeneration = Number(session._sdkQueryGeneration || 0);
1449
+ session._continuationStartupProbe = {
1450
+ queryInstance: acceptedHandle,
1451
+ queryGeneration: acceptedGeneration,
1452
+ resolve: resolveStartup,
1453
+ onLateStartup: onLateStartup,
1454
+ timedOut: false,
1455
+ };
1456
+ session._continuationStartupProbe.timer = setTimeout(function () {
1457
+ var probe = session._continuationStartupProbe;
1458
+ if (!probe || probe.queryInstance !== acceptedHandle || probe.queryGeneration !== acceptedGeneration) return;
1459
+ probe.timedOut = true;
1460
+ probe.resolve({ accepted: false, timedOut: true, reason: "The successor did not initialize in time." });
1461
+ }, Number(opts.continuationStartupWaitMs || 15000));
1462
+ });
1463
+ var initialAccepted = result === true && !!acceptedHandle;
1464
+ if (!initialAccepted) resolveStartup({ accepted: false, reason: "The successor did not accept its initial query." });
1465
+ var startupResult = await startup;
1466
+ var accepted = initialAccepted && startupResult.accepted === true;
1467
+ return {
1468
+ accepted: accepted,
1469
+ initialAccepted: initialAccepted,
1470
+ queryAlive: session.queryInstance === acceptedHandle,
1471
+ queryGeneration: acceptedGeneration,
1472
+ timedOut: startupResult.timedOut === true,
1473
+ reason: accepted ? null : startupResult.reason || "The successor did not initialize its accepted query.",
1474
+ };
1475
+ }
1476
+
1401
1477
  async function startQueryInner(session, text, images, linuxUser, beforePush, onAccepted) {
1402
1478
  var sessionRuntimeEnv = getRuntimeEnv(session);
1403
1479
  var resumedAtQueryStart = !!session.cliSessionId;
@@ -2647,6 +2723,7 @@ function createSDKBridge(opts) {
2647
2723
  rollbackConversation: rollbackConversation,
2648
2724
  forkSession: forkSessionUnified,
2649
2725
  startQuery: startQuery,
2726
+ startQueryWithAcceptance: startQueryWithAcceptance,
2650
2727
  pushMessage: pushMessage,
2651
2728
  refreshSessionRuntime: refreshSessionRuntime,
2652
2729
  refreshEnvironmentRuntime: refreshEnvironmentRuntime,