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.
- 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 +82 -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/project-connection.js +1 -1
- package/lib/project-driver-continuation.js +499 -0
- package/lib/project-message-delivery.js +20 -0
- package/lib/project-pair-lifecycle.js +73 -73
- package/lib/project-pair-replacement-state.js +5 -1
- package/lib/project-session-handoff.js +48 -55
- package/lib/project-user-message.js +21 -0
- package/lib/project-worker-proposal.js +1 -1
- package/lib/project.js +49 -2
- package/lib/public/css/driver-continuation.css +56 -0
- package/lib/public/css/overlays.css +44 -16
- package/lib/public/css/pane.css +9 -5
- package/lib/public/index.html +3 -3
- package/lib/public/modules/app-connection.js +169 -51
- package/lib/public/modules/app-messages.js +19 -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/permission-control.js +8 -3
- package/lib/public/modules/websocket-lifecycle.js +178 -0
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +77 -0
- package/lib/session-handoff-discovery.js +443 -0
- package/lib/session-handoff-mcp-server.js +32 -4
- package/lib/session-pair-prompts.js +5 -0
- package/lib/session-pair-turn-control.js +19 -3
- package/lib/session-split-group-anchors.js +44 -0
- package/lib/session-split-groups.js +209 -59
- package/lib/sessions.js +21 -3
- package/lib/ws-schema.js +6 -0
- 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 };
|
package/lib/public/style.css
CHANGED
|
@@ -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,
|