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
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
// Bounded, query-bound discovery of other visible Driver sessions.
|
|
2
|
+
|
|
3
|
+
var sessionDriverEligibility = require("./session-driver-eligibility");
|
|
4
|
+
var sessionRef = require("./workspace-query-service").sessionRef;
|
|
5
|
+
|
|
6
|
+
var MAX_RESULTS = 20;
|
|
7
|
+
var MAX_QUERY_CHARS = 200;
|
|
8
|
+
var MAX_EXCERPT_CHARS = 1200;
|
|
9
|
+
var MAX_RESPONSE_CHARS = 15000;
|
|
10
|
+
var MAX_QUERY_RESPONSE_CHARS = 32000;
|
|
11
|
+
var MAX_CALLS = 8;
|
|
12
|
+
var UNVERSIONED_QUERY = "__unversioned_query__";
|
|
13
|
+
var CROSS_PROJECT_LIMITATION = "Only sessions in this project are available.";
|
|
14
|
+
var EXHAUSTED_TEXT = "Retrieval budget exhausted for this query.";
|
|
15
|
+
|
|
16
|
+
function clean(value, limit) {
|
|
17
|
+
if (typeof value !== "string") return "";
|
|
18
|
+
var result = value.replace(/\u0000/g, "").trim();
|
|
19
|
+
if (result.length <= limit) return result;
|
|
20
|
+
return result.substring(0, Math.max(0, limit - 3)) + "...";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resultResponse(text) {
|
|
24
|
+
return { content: [{ type: "text", text: text }] };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function errorResponse(text) {
|
|
28
|
+
return { content: [{ type: "text", text: "Error: " + text }], isError: true };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function responseSize(response) {
|
|
32
|
+
return JSON.stringify(response).length;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isExcludedDriverIdentity(session) {
|
|
36
|
+
return !!(session && (
|
|
37
|
+
session.hidden === true || session.delegated === true ||
|
|
38
|
+
session._pairDelegation || session._delegatedBy ||
|
|
39
|
+
session.scheduledTaskRun || session.projectLogReview
|
|
40
|
+
));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function currentGeneration(session) {
|
|
44
|
+
if (!session) return null;
|
|
45
|
+
if (Number.isInteger(session._sdkQueryGeneration)) return session._sdkQueryGeneration;
|
|
46
|
+
if (Number.isInteger(session._queryGeneration)) return session._queryGeneration;
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function entryKey(projectSlug, source, index) {
|
|
51
|
+
return "entry:" + sessionRef(projectSlug, source) + ":" + index;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function timestamp(entry, source) {
|
|
55
|
+
var value = entry && entry._ts;
|
|
56
|
+
if (typeof value !== "number" || !Number.isFinite(value)) value = source.lastActivity || source.createdAt || 0;
|
|
57
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function attachSessionHandoffDiscovery(ctx) {
|
|
61
|
+
var sm = ctx.sm;
|
|
62
|
+
var budgets = new WeakMap();
|
|
63
|
+
|
|
64
|
+
function isDriver(session) {
|
|
65
|
+
if (!sessionDriverEligibility.isEligibleDriverSession(session)) return false;
|
|
66
|
+
if (isExcludedDriverIdentity(session) || session.isMate === true) return false;
|
|
67
|
+
if (typeof ctx.isDriverOperatedSession !== "function") return true;
|
|
68
|
+
try {
|
|
69
|
+
return ctx.isDriverOperatedSession(session) !== true;
|
|
70
|
+
} catch (e) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function stateFor(boundSession, generation) {
|
|
76
|
+
var current = currentGeneration(boundSession);
|
|
77
|
+
if (generation === UNVERSIONED_QUERY && current !== null) return null;
|
|
78
|
+
if (generation !== UNVERSIONED_QUERY && generation !== undefined && generation !== null && current !== generation) return null;
|
|
79
|
+
var key = generation !== undefined && generation !== null
|
|
80
|
+
? generation
|
|
81
|
+
: (current === null ? "unversioned" : current);
|
|
82
|
+
var state = budgets.get(boundSession);
|
|
83
|
+
if (!state || state.key !== key) {
|
|
84
|
+
state = { key: key, calls: 0, used: 0, seen: Object.create(null), exhausted: false };
|
|
85
|
+
budgets.set(boundSession, state);
|
|
86
|
+
}
|
|
87
|
+
return state;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function authorize(boundSession, candidate) {
|
|
91
|
+
if (!boundSession || !sm || !sm.sessions) return false;
|
|
92
|
+
if (sm.sessions.get(boundSession.localId) !== boundSession) return false;
|
|
93
|
+
if (ctx.isMate || !isDriver(boundSession) || typeof ctx.getProjectAccess !== "function") return false;
|
|
94
|
+
var access;
|
|
95
|
+
try {
|
|
96
|
+
access = ctx.getProjectAccess();
|
|
97
|
+
} catch (e) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
if (!access || access.error) return false;
|
|
101
|
+
var actorId = boundSession.ownerId || null;
|
|
102
|
+
if (typeof ctx.isMultiUser === "function" && ctx.isMultiUser()) {
|
|
103
|
+
if (!actorId || typeof ctx.findUserById !== "function" || typeof ctx.canAccessSession !== "function") return false;
|
|
104
|
+
var actor = ctx.findUserById(actorId);
|
|
105
|
+
if (!actor || actor.active === false || actor.disabled === true) return false;
|
|
106
|
+
try {
|
|
107
|
+
if (ctx.canAccessSession(actorId, boundSession, access) !== true) return false;
|
|
108
|
+
if (candidate === boundSession) return true;
|
|
109
|
+
return !!candidate && isDriver(candidate) && ctx.canAccessSession(actorId, candidate, access) === true;
|
|
110
|
+
} catch (e2) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!candidate || !isDriver(candidate)) return false;
|
|
115
|
+
if (candidate === boundSession) return true;
|
|
116
|
+
return !candidate.ownerId || candidate.ownerId === actorId;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function candidates(boundSession) {
|
|
120
|
+
if (!authorize(boundSession, boundSession)) return [];
|
|
121
|
+
var result = [];
|
|
122
|
+
sm.sessions.forEach(function (candidate) {
|
|
123
|
+
if (candidate !== boundSession && authorize(boundSession, candidate)) result.push(candidate);
|
|
124
|
+
});
|
|
125
|
+
result.sort(function (a, b) {
|
|
126
|
+
var activity = (b.lastActivity || b.createdAt || 0) - (a.lastActivity || a.createdAt || 0);
|
|
127
|
+
if (activity) return activity;
|
|
128
|
+
return sessionRef(ctx.projectSlug, a) < sessionRef(ctx.projectSlug, b) ? -1 : 1;
|
|
129
|
+
});
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function metadata(boundSession, candidate) {
|
|
134
|
+
if (!authorize(boundSession, candidate)) return null;
|
|
135
|
+
return {
|
|
136
|
+
sessionRef: sessionRef(ctx.projectSlug, candidate),
|
|
137
|
+
title: clean(candidate.title || "New Session", 160),
|
|
138
|
+
vendor: clean(candidate.vendor || "", 40) || null,
|
|
139
|
+
createdAt: candidate.createdAt || 0,
|
|
140
|
+
lastActivity: candidate.lastActivity || candidate.createdAt || 0,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function refusal(message) {
|
|
145
|
+
return errorResponse(message || EXHAUSTED_TEXT);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function beginCall(boundSession, generation) {
|
|
149
|
+
var state = stateFor(boundSession, generation);
|
|
150
|
+
if (!state) return { refusal: refusal("This retrieval tool belongs to an older query.") };
|
|
151
|
+
if (state.exhausted || state.calls >= MAX_CALLS) {
|
|
152
|
+
state.exhausted = true;
|
|
153
|
+
return { state: state, refusal: refusal(EXHAUSTED_TEXT) };
|
|
154
|
+
}
|
|
155
|
+
state.calls++;
|
|
156
|
+
if (state.used >= MAX_QUERY_RESPONSE_CHARS) {
|
|
157
|
+
state.exhausted = true;
|
|
158
|
+
return { state: state, refusal: refusal(EXHAUSTED_TEXT) };
|
|
159
|
+
}
|
|
160
|
+
return { state: state };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function budgetMetadata(state) {
|
|
164
|
+
return {
|
|
165
|
+
maxResponseChars: MAX_RESPONSE_CHARS,
|
|
166
|
+
maxQueryChars: MAX_QUERY_RESPONSE_CHARS,
|
|
167
|
+
usedChars: state.used,
|
|
168
|
+
maxCalls: MAX_CALLS,
|
|
169
|
+
calls: state.calls,
|
|
170
|
+
query: state.key,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function encodePayload(state, payload) {
|
|
175
|
+
var response;
|
|
176
|
+
var size = 0;
|
|
177
|
+
for (var i = 0; i < 8; i++) {
|
|
178
|
+
response = resultResponse(JSON.stringify(payload));
|
|
179
|
+
size = responseSize(response);
|
|
180
|
+
if (!payload.budget || payload.budget.usedChars === state.used + size) break;
|
|
181
|
+
payload.budget.usedChars = state.used + size;
|
|
182
|
+
}
|
|
183
|
+
return { response: response, size: size };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function finishPayload(call, payload, arrayName, keys) {
|
|
187
|
+
var state = call.state;
|
|
188
|
+
var available = Math.min(MAX_RESPONSE_CHARS, MAX_QUERY_RESPONSE_CHARS - state.used);
|
|
189
|
+
var encoded = encodePayload(state, payload);
|
|
190
|
+
while (encoded.size > available && arrayName && payload[arrayName].length > 0) {
|
|
191
|
+
payload[arrayName].pop();
|
|
192
|
+
keys.pop();
|
|
193
|
+
payload.truncated = true;
|
|
194
|
+
encoded = encodePayload(state, payload);
|
|
195
|
+
}
|
|
196
|
+
if (encoded.size > available) {
|
|
197
|
+
state.exhausted = true;
|
|
198
|
+
return refusal(EXHAUSTED_TEXT);
|
|
199
|
+
}
|
|
200
|
+
state.used += encoded.size;
|
|
201
|
+
for (var i = 0; i < keys.length; i++) state.seen[keys[i]] = true;
|
|
202
|
+
if (state.used >= MAX_QUERY_RESPONSE_CHARS || state.calls >= MAX_CALLS) state.exhausted = true;
|
|
203
|
+
return encoded.response;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function finishError(call, message) {
|
|
207
|
+
if (call.refusal) return call.refusal;
|
|
208
|
+
var state = call.state;
|
|
209
|
+
var response = errorResponse(clean(message || "Retrieval failed.", 500));
|
|
210
|
+
var size = responseSize(response);
|
|
211
|
+
if (size > MAX_QUERY_RESPONSE_CHARS - state.used) {
|
|
212
|
+
state.exhausted = true;
|
|
213
|
+
return refusal(EXHAUSTED_TEXT);
|
|
214
|
+
}
|
|
215
|
+
state.used += size;
|
|
216
|
+
if (state.used >= MAX_QUERY_RESPONSE_CHARS || state.calls >= MAX_CALLS) state.exhausted = true;
|
|
217
|
+
return response;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function basePayload(state) {
|
|
221
|
+
return {
|
|
222
|
+
projectSlug: ctx.projectSlug,
|
|
223
|
+
crossProject: false,
|
|
224
|
+
limitation: CROSS_PROJECT_LIMITATION,
|
|
225
|
+
budget: budgetMetadata(state),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function list(boundSession, args, generation) {
|
|
230
|
+
var call = beginCall(boundSession, generation);
|
|
231
|
+
if (call.refusal) return call.refusal;
|
|
232
|
+
if (!authorize(boundSession, boundSession)) return finishError(call, "Driver session access denied.");
|
|
233
|
+
var all = candidates(boundSession);
|
|
234
|
+
var payload = basePayload(call.state);
|
|
235
|
+
payload.sessions = [];
|
|
236
|
+
payload.scannedSessions = all.length;
|
|
237
|
+
payload.truncated = false;
|
|
238
|
+
payload.budget.maxResults = MAX_RESULTS;
|
|
239
|
+
var keys = [];
|
|
240
|
+
for (var i = 0; i < all.length && payload.sessions.length < MAX_RESULTS; i++) {
|
|
241
|
+
var item = metadata(boundSession, all[i]);
|
|
242
|
+
var key = item ? "source:" + item.sessionRef : null;
|
|
243
|
+
if (!item || call.state.seen[key]) continue;
|
|
244
|
+
payload.sessions.push(item);
|
|
245
|
+
keys.push(key);
|
|
246
|
+
}
|
|
247
|
+
payload.truncated = all.length > payload.sessions.length;
|
|
248
|
+
return finishPayload(call, payload, "sessions", keys);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function search(boundSession, args, generation) {
|
|
252
|
+
var call = beginCall(boundSession, generation);
|
|
253
|
+
if (call.refusal) return call.refusal;
|
|
254
|
+
if (!authorize(boundSession, boundSession)) return finishError(call, "Driver session access denied.");
|
|
255
|
+
var query = clean(args && args.query || "", MAX_QUERY_CHARS).toLowerCase();
|
|
256
|
+
if (!query) return finishError(call, "A keyword query is required.");
|
|
257
|
+
var all = candidates(boundSession);
|
|
258
|
+
var payload = basePayload(call.state);
|
|
259
|
+
payload.query = query;
|
|
260
|
+
payload.results = [];
|
|
261
|
+
payload.scannedSessions = all.length;
|
|
262
|
+
payload.truncated = false;
|
|
263
|
+
payload.budget.maxResults = MAX_RESULTS;
|
|
264
|
+
payload.budget.maxExcerptChars = MAX_EXCERPT_CHARS;
|
|
265
|
+
var keys = [];
|
|
266
|
+
for (var i = 0; i < all.length && payload.results.length < MAX_RESULTS; i++) {
|
|
267
|
+
var candidate = all[i];
|
|
268
|
+
var item = metadata(boundSession, candidate);
|
|
269
|
+
if (!item) continue;
|
|
270
|
+
var history = Array.isArray(candidate.history) ? candidate.history : [];
|
|
271
|
+
for (var h = 0; h < history.length && payload.results.length < MAX_RESULTS; h++) {
|
|
272
|
+
var entry = history[h];
|
|
273
|
+
if (!entry || (entry.type !== "user_message" && entry.type !== "delta")) continue;
|
|
274
|
+
if (typeof entry.text !== "string") continue;
|
|
275
|
+
var match = entry.text.toLowerCase().indexOf(query);
|
|
276
|
+
if (match === -1) continue;
|
|
277
|
+
var key = entryKey(ctx.projectSlug, candidate, h);
|
|
278
|
+
if (call.state.seen[key]) continue;
|
|
279
|
+
var start = Math.max(0, match - 180);
|
|
280
|
+
var excerpt = clean(entry.text.substring(start, start + MAX_EXCERPT_CHARS), MAX_EXCERPT_CHARS);
|
|
281
|
+
if (start > 0) excerpt = "..." + excerpt;
|
|
282
|
+
var date = timestamp(entry, candidate);
|
|
283
|
+
payload.results.push({
|
|
284
|
+
sessionRef: item.sessionRef,
|
|
285
|
+
title: item.title,
|
|
286
|
+
vendor: item.vendor,
|
|
287
|
+
role: entry.type === "user_message" ? "user" : "assistant",
|
|
288
|
+
entryIndex: h,
|
|
289
|
+
date: date,
|
|
290
|
+
excerpt: excerpt,
|
|
291
|
+
citation: item.sessionRef + " entry " + h + " (" + new Date(date).toISOString() + ")",
|
|
292
|
+
});
|
|
293
|
+
keys.push(key);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
payload.truncated = payload.results.length >= MAX_RESULTS;
|
|
297
|
+
return finishPayload(call, payload, "results", keys);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function read(boundSession, args, generation) {
|
|
301
|
+
var call = beginCall(boundSession, generation);
|
|
302
|
+
if (call.refusal) return call.refusal;
|
|
303
|
+
if (!authorize(boundSession, boundSession)) return finishError(call, "Driver session access denied.");
|
|
304
|
+
var ref = args && args.sessionRef;
|
|
305
|
+
var all = candidates(boundSession);
|
|
306
|
+
var candidate = null;
|
|
307
|
+
for (var i = 0; i < all.length; i++) {
|
|
308
|
+
if (sessionRef(ctx.projectSlug, all[i]) === ref) candidate = all[i];
|
|
309
|
+
}
|
|
310
|
+
if (!candidate || !authorize(boundSession, candidate)) {
|
|
311
|
+
return finishError(call, "Session not found or not shared with this Driver.");
|
|
312
|
+
}
|
|
313
|
+
var item = metadata(boundSession, candidate);
|
|
314
|
+
var history = Array.isArray(candidate.history) ? candidate.history : [];
|
|
315
|
+
var limit = Number.isFinite(args && args.limit) ? Math.max(1, Math.min(20, Math.floor(args.limit))) : 5;
|
|
316
|
+
var start;
|
|
317
|
+
if (Number.isInteger(args && args.entryIndex)) start = Math.max(0, args.entryIndex - 2);
|
|
318
|
+
else if (Number.isInteger(args && args.offset)) start = Math.max(0, args.offset);
|
|
319
|
+
else start = 0;
|
|
320
|
+
start = Math.min(history.length, start);
|
|
321
|
+
var end = Math.min(history.length, start + limit);
|
|
322
|
+
var payload = basePayload(call.state);
|
|
323
|
+
payload.session = item;
|
|
324
|
+
payload.excerpts = [];
|
|
325
|
+
payload.requestedStart = start;
|
|
326
|
+
payload.requestedEnd = end;
|
|
327
|
+
payload.truncated = end < history.length;
|
|
328
|
+
payload.budget.maxExcerptChars = MAX_EXCERPT_CHARS;
|
|
329
|
+
var keys = [];
|
|
330
|
+
for (var h = start; h < end; h++) {
|
|
331
|
+
var entry = history[h];
|
|
332
|
+
if (!entry || (entry.type !== "user_message" && entry.type !== "delta")) continue;
|
|
333
|
+
if (typeof entry.text !== "string") continue;
|
|
334
|
+
var key = entryKey(ctx.projectSlug, candidate, h);
|
|
335
|
+
if (call.state.seen[key]) continue;
|
|
336
|
+
var date = timestamp(entry, candidate);
|
|
337
|
+
payload.excerpts.push({
|
|
338
|
+
entryIndex: h,
|
|
339
|
+
role: entry.type === "user_message" ? "user" : "assistant",
|
|
340
|
+
date: date,
|
|
341
|
+
excerpt: clean(entry.text, MAX_EXCERPT_CHARS),
|
|
342
|
+
citation: item.sessionRef + " entry " + h,
|
|
343
|
+
});
|
|
344
|
+
keys.push(key);
|
|
345
|
+
}
|
|
346
|
+
return finishPayload(call, payload, "excerpts", keys);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function legacyLine(entry) {
|
|
350
|
+
if (!entry) return null;
|
|
351
|
+
var label;
|
|
352
|
+
var text = "";
|
|
353
|
+
if (entry.type === "user_message") {
|
|
354
|
+
label = "USER";
|
|
355
|
+
text = entry.text || "";
|
|
356
|
+
} else if (entry.type === "delta") {
|
|
357
|
+
label = "ASSISTANT";
|
|
358
|
+
text = entry.text || "";
|
|
359
|
+
} else if (entry.type === "tool_executing" || entry.type === "tool_result") {
|
|
360
|
+
label = "TOOL";
|
|
361
|
+
var input = "";
|
|
362
|
+
try {
|
|
363
|
+
input = entry.input ? JSON.stringify(entry.input) || "" : "";
|
|
364
|
+
} catch (e) {
|
|
365
|
+
input = "[unserializable input]";
|
|
366
|
+
}
|
|
367
|
+
text = (entry.name || "") + (input ? " " + input.substring(0, 120) : "");
|
|
368
|
+
} else {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
return "[" + label + "] " + clean(text, 800);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function legacy(boundSession, source, args, generation) {
|
|
375
|
+
var call = beginCall(boundSession, generation);
|
|
376
|
+
if (call.refusal) return call.refusal;
|
|
377
|
+
if (!authorize(boundSession, boundSession) || !authorize(boundSession, source)) {
|
|
378
|
+
return finishError(call, "Source session access denied.");
|
|
379
|
+
}
|
|
380
|
+
var history = Array.isArray(source.history) ? source.history : [];
|
|
381
|
+
var limit = Number.isFinite(args && args.limit) ? Math.max(1, Math.min(100, Math.floor(args.limit))) : 30;
|
|
382
|
+
var offset = Number.isFinite(args && args.offset)
|
|
383
|
+
? Math.max(0, Math.min(history.length, Math.floor(args.offset)))
|
|
384
|
+
: Math.max(0, history.length - limit);
|
|
385
|
+
var end = Math.min(history.length, offset + limit);
|
|
386
|
+
var header = "# " + clean(source.title || "Untitled session", 160) + " — " +
|
|
387
|
+
clean(source.vendor || "unknown", 40) + "/" + source.localId + "\n" +
|
|
388
|
+
"Showing entries " + (history.length ? offset + 1 : 0) + "-" + end + " of " + history.length + "\n";
|
|
389
|
+
var lines = [];
|
|
390
|
+
var keys = [];
|
|
391
|
+
for (var i = offset; i < end; i++) {
|
|
392
|
+
var line = legacyLine(history[i]);
|
|
393
|
+
var key = entryKey(ctx.projectSlug, source, i);
|
|
394
|
+
if (!line || call.state.seen[key]) continue;
|
|
395
|
+
lines.push(line);
|
|
396
|
+
keys.push(key);
|
|
397
|
+
}
|
|
398
|
+
var available = Math.min(MAX_RESPONSE_CHARS, MAX_QUERY_RESPONSE_CHARS - call.state.used);
|
|
399
|
+
var response = resultResponse(header + "\n" + lines.join("\n"));
|
|
400
|
+
while (responseSize(response) > available && lines.length > 0) {
|
|
401
|
+
lines.pop();
|
|
402
|
+
keys.pop();
|
|
403
|
+
response = resultResponse(header + "\n" + lines.join("\n") + "\n[truncated]");
|
|
404
|
+
}
|
|
405
|
+
var size = responseSize(response);
|
|
406
|
+
if (size > available) {
|
|
407
|
+
call.state.exhausted = true;
|
|
408
|
+
return refusal(EXHAUSTED_TEXT);
|
|
409
|
+
}
|
|
410
|
+
call.state.used += size;
|
|
411
|
+
for (var k = 0; k < keys.length; k++) call.state.seen[keys[k]] = true;
|
|
412
|
+
if (call.state.used >= MAX_QUERY_RESPONSE_CHARS || call.state.calls >= MAX_CALLS) call.state.exhausted = true;
|
|
413
|
+
return response;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function fail(boundSession, generation, message) {
|
|
417
|
+
var call = beginCall(boundSession, generation);
|
|
418
|
+
return finishError(call, message);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
authorize: authorize,
|
|
423
|
+
captureGeneration: function (session) {
|
|
424
|
+
var generation = currentGeneration(session);
|
|
425
|
+
return generation === null ? UNVERSIONED_QUERY : generation;
|
|
426
|
+
},
|
|
427
|
+
currentGeneration: currentGeneration,
|
|
428
|
+
fail: fail,
|
|
429
|
+
legacy: legacy,
|
|
430
|
+
list: list,
|
|
431
|
+
read: read,
|
|
432
|
+
search: search,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
module.exports = {
|
|
437
|
+
attachSessionHandoffDiscovery: attachSessionHandoffDiscovery,
|
|
438
|
+
MAX_CALLS: MAX_CALLS,
|
|
439
|
+
MAX_QUERY_RESPONSE_CHARS: MAX_QUERY_RESPONSE_CHARS,
|
|
440
|
+
MAX_RESPONSE_CHARS: MAX_RESPONSE_CHARS,
|
|
441
|
+
MAX_RESULTS: MAX_RESULTS,
|
|
442
|
+
UNVERSIONED_QUERY: UNVERSIONED_QUERY,
|
|
443
|
+
};
|
|
@@ -5,12 +5,16 @@ var buildShape = require("./session-spawn-mcp-server").buildShape;
|
|
|
5
5
|
var TOOL_DESCRIPTION =
|
|
6
6
|
"This session was continued from another agent's session via a context snapshot. " +
|
|
7
7
|
"That snapshot omitted tool calls and older turns. Read the original Clay session record directly, including user messages, assistant text, and summarized tool calls, regardless of which vendor produced it.";
|
|
8
|
+
var BUDGET_DESCRIPTION =
|
|
9
|
+
" Responses are capped at 15,000 serialized characters, 32,000 serialized characters and 8 calls per query. " +
|
|
10
|
+
"After exhaustion, calls return one constant refusal; repeated refusals are outside the aggregate cap, so callers must stop.";
|
|
8
11
|
|
|
9
12
|
function getToolDefs(handlers) {
|
|
10
|
-
|
|
13
|
+
var tools = [];
|
|
14
|
+
if (handlers.read) tools.push(
|
|
11
15
|
{
|
|
12
16
|
name: "read_handoff_source",
|
|
13
|
-
description: TOOL_DESCRIPTION,
|
|
17
|
+
description: TOOL_DESCRIPTION + BUDGET_DESCRIPTION,
|
|
14
18
|
inputSchema: buildShape({
|
|
15
19
|
offset: {
|
|
16
20
|
type: "number",
|
|
@@ -26,8 +30,32 @@ function getToolDefs(handlers) {
|
|
|
26
30
|
},
|
|
27
31
|
}),
|
|
28
32
|
handler: function (args) { return handlers.read(args || {}); },
|
|
29
|
-
}
|
|
30
|
-
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
if (handlers.listOtherDrivers) tools.push({
|
|
36
|
+
name: "list_other_driver_sessions",
|
|
37
|
+
description: "List bounded metadata for other visible Driver sessions in this project. Shared-session access is checked at call time; cross-project discovery is unavailable." + BUDGET_DESCRIPTION,
|
|
38
|
+
inputSchema: buildShape({ limit: { type: "number", description: "Ignored beyond the server cap of 20." } }),
|
|
39
|
+
handler: function (args) { return handlers.listOtherDrivers(args || {}); },
|
|
40
|
+
});
|
|
41
|
+
if (handlers.searchOtherDrivers) tools.push({
|
|
42
|
+
name: "search_other_driver_sessions",
|
|
43
|
+
description: "Search user and assistant text in other visible Driver sessions in this project, returning bounded excerpts with session, entry, and date citations. Cross-project search is unavailable." + BUDGET_DESCRIPTION,
|
|
44
|
+
inputSchema: buildShape({ query: { type: "string", description: "Keyword to search." } }),
|
|
45
|
+
handler: function (args) { return handlers.searchOtherDrivers(args || {}); },
|
|
46
|
+
});
|
|
47
|
+
if (handlers.readOtherDriver) tools.push({
|
|
48
|
+
name: "read_other_driver_session",
|
|
49
|
+
description: "Read a bounded history window from one visible other Driver session by opaque session reference. Already returned entries are skipped without dropping unseen entries from the requested window. Cross-project reads are unavailable." + BUDGET_DESCRIPTION,
|
|
50
|
+
inputSchema: buildShape({
|
|
51
|
+
sessionRef: { type: "string", description: "Opaque session reference from discovery or search." },
|
|
52
|
+
entryIndex: { type: "number", description: "Center a window near this cited history entry." },
|
|
53
|
+
offset: { type: "number", description: "Start the window at this history entry when entryIndex is omitted." },
|
|
54
|
+
limit: { type: "number", description: "History positions in the window. Defaults to 5 and is capped at 20." },
|
|
55
|
+
}),
|
|
56
|
+
handler: function (args) { return handlers.readOtherDriver(args || {}); },
|
|
57
|
+
});
|
|
58
|
+
return tools;
|
|
31
59
|
}
|
|
32
60
|
|
|
33
61
|
module.exports = {
|
|
@@ -34,6 +34,7 @@ var driverEligibility = require("./session-driver-eligibility");
|
|
|
34
34
|
var models = require("./project-models");
|
|
35
35
|
var yoke = require("./yoke");
|
|
36
36
|
var sessionProvenance = require("./session-provenance");
|
|
37
|
+
var splitRoles = require("./session-split-group-roles");
|
|
37
38
|
|
|
38
39
|
function attachPairFactory(ctx) {
|
|
39
40
|
var sm = ctx.sm;
|
|
@@ -230,6 +231,69 @@ function attachPairFactory(ctx) {
|
|
|
230
231
|
return created;
|
|
231
232
|
}
|
|
232
233
|
|
|
234
|
+
function createUnpairedWorkerForDriver(driver, args) {
|
|
235
|
+
var runtime = preflightWorkerForDriver(driver, args);
|
|
236
|
+
var worker = sm.createSessionRaw({
|
|
237
|
+
ownerId: driver.ownerId || null,
|
|
238
|
+
vendor: runtime.vendor,
|
|
239
|
+
model: runtime.model,
|
|
240
|
+
effort: runtime.effort,
|
|
241
|
+
});
|
|
242
|
+
try {
|
|
243
|
+
worker.title = "Split Worker · " + ((yoke.getVendorInfo(runtime.vendor) || {}).displayName || runtime.vendor);
|
|
244
|
+
if (runtime.explicitEffort) worker.loopSettings = { effort: runtime.effort };
|
|
245
|
+
sessionProvenance.markWorker(driver, worker, sm.sessions);
|
|
246
|
+
if (typeof sm.saveSessionFile === "function") {
|
|
247
|
+
if (sm.saveSessionFile(driver) === false || sm.saveSessionFile(worker) === false) {
|
|
248
|
+
throw new Error("Split Worker session persistence returned false");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return worker;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
removeCreated(worker);
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function exactMutation(driver, worker, args, method) {
|
|
259
|
+
var group = store.groupForMember(driver.localId);
|
|
260
|
+
if (!group) throw new Error("split group not found");
|
|
261
|
+
var role = splitRoles.normalizePair(group.pair, group.members);
|
|
262
|
+
if (!role.ok || role.kind === "adhoc") throw new Error("split group roles changed");
|
|
263
|
+
var created = createUnpairedWorkerForDriver(driver, args);
|
|
264
|
+
var request = {
|
|
265
|
+
groupId: group.id,
|
|
266
|
+
driverId: driver.localId,
|
|
267
|
+
driverOriginId: driver.sessionOriginId || null,
|
|
268
|
+
expectedGroup: args.expectedGroup || group,
|
|
269
|
+
expectedWorkerIds: Array.isArray(args.expectedWorkerIds) ? args.expectedWorkerIds.slice() : role.workerIds.slice(),
|
|
270
|
+
newWorkerId: created.localId,
|
|
271
|
+
newWorkerOriginId: created.sessionOriginId || null,
|
|
272
|
+
};
|
|
273
|
+
if (worker) {
|
|
274
|
+
request.workerId = worker.localId;
|
|
275
|
+
request.workerOriginId = worker.sessionOriginId || null;
|
|
276
|
+
request.generation = worker._pairGeneration ||
|
|
277
|
+
worker.sessionProvenance && worker.sessionProvenance.generation || null;
|
|
278
|
+
}
|
|
279
|
+
var result = store[method](driver.ownerId || null, request, ctx.multiWorkerFeature);
|
|
280
|
+
if (!result.ok) {
|
|
281
|
+
removeCreated(created);
|
|
282
|
+
throw new Error(result.error);
|
|
283
|
+
}
|
|
284
|
+
sm.broadcastSessionList();
|
|
285
|
+
sm.sendToSession(driver, { type: "pair_session_created", ok: true, group: result.group });
|
|
286
|
+
return { driver: driver, worker: created, group: result.group };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function addWorkerForDriver(driver, args) {
|
|
290
|
+
return exactMutation(driver, null, args, "addWorker");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function replaceWorkerForDriver(driver, worker, args) {
|
|
294
|
+
return exactMutation(driver, worker, args, "replaceWorker");
|
|
295
|
+
}
|
|
296
|
+
|
|
233
297
|
function createOwnedPair(ownerId, args) {
|
|
234
298
|
if (ctx.isMate) throw new Error("Pair sessions are only available in projects");
|
|
235
299
|
var request = args || {};
|
|
@@ -276,6 +340,8 @@ function attachPairFactory(ctx) {
|
|
|
276
340
|
createPair: createPair,
|
|
277
341
|
createPairRecord: createPairRecord,
|
|
278
342
|
createWorkerForDriver: createWorkerForDriver,
|
|
343
|
+
addWorkerForDriver: addWorkerForDriver,
|
|
344
|
+
replaceWorkerForDriver: replaceWorkerForDriver,
|
|
279
345
|
createOwnedPair: createOwnedPair,
|
|
280
346
|
removeOwnedPair: removeOwnedPair,
|
|
281
347
|
preflightRuntime: preflightRuntime,
|
|
@@ -19,31 +19,33 @@ function getToolDefs(handlers, options) {
|
|
|
19
19
|
description: "Delegate one concrete follow-up task to the existing visible Split Worker. This tool is exposed only after the user has configured and accepted an exact pair. A completed turn does not end the Split Worker session: reuse the same Split Worker for follow-up implementation and corrections instead of taking over its work. Detached completions are pushed back automatically. A delegated turn cannot delegate back, so keep orchestration one hop deep.",
|
|
20
20
|
inputSchema: buildShape({
|
|
21
21
|
message: { type: "string", description: "The complete task or question for the partner." },
|
|
22
|
-
wait: { type: "boolean", description: "Wait for the partner's turn to finish. Defaults to true." },
|
|
22
|
+
wait: { type: "boolean", description: "Wait for the partner's turn to finish. Defaults to false; set true for an explicit blocking wait." },
|
|
23
23
|
timeoutSeconds: { type: "number", description: "Maximum wait in seconds, from 1 to 900. Defaults to 300." },
|
|
24
24
|
operationId: { type: "string", description: "Optional stable id for this delegation. Reusing it within the same human turn returns the original operation instead of sending twice." },
|
|
25
25
|
taskId: { type: "string", description: "Optional stable task id for generation and completion correlation." },
|
|
26
|
+
workerId: { type: "number", description: "Optional exact Worker session id; required when multiple Workers are configured." },
|
|
26
27
|
}, ["message"]),
|
|
27
28
|
handler: function (args) { return handlers.send(args || {}); },
|
|
28
29
|
},
|
|
29
30
|
{
|
|
30
31
|
name: "read_partner",
|
|
31
|
-
description: "Read the Split Worker's current status and recent conversation turns, plus the same bounded capacity report partner_status returns. Use
|
|
32
|
+
description: "Read the Split Worker's current status and recent conversation turns, plus the same bounded capacity report partner_status returns. Use only for a requested status, concrete failure or stall diagnosis, or capacity/queue decision; do not poll routinely. Completed results are pushed automatically.",
|
|
32
33
|
inputSchema: buildShape({
|
|
33
34
|
lastTurns: { type: "number", description: "Number of recent user-message-delimited turns, from 0 to 5. Use 0 for status only. Defaults to 1." },
|
|
35
|
+
workerId: { type: "number", description: "Optional exact Worker session id; required when multiple Workers are configured." },
|
|
34
36
|
}),
|
|
35
37
|
handler: function (args) { return handlers.read(args || {}); },
|
|
36
38
|
},
|
|
37
39
|
{
|
|
38
40
|
name: "interrupt_partner",
|
|
39
41
|
description: "Interrupt the Split Worker's current task. Use this when the task is going in the wrong direction, needs to be reprioritized, or must stop before the next instruction. The Split Worker returns any partial result to you for review.",
|
|
40
|
-
inputSchema: buildShape({ reason: { type: "string", description: "Why the Driver is interrupting this task." } }),
|
|
42
|
+
inputSchema: buildShape({ reason: { type: "string", description: "Why the Driver is interrupting this task." }, workerId: { type: "number", description: "Optional exact Worker session id; required when multiple Workers are configured." } }),
|
|
41
43
|
handler: function (args) { return handlers.interrupt(args || {}); },
|
|
42
44
|
},
|
|
43
45
|
{
|
|
44
46
|
name: "close_partner",
|
|
45
47
|
description: "Close the visible Split Worker pane and dissolve the Driver/Split Worker pair while preserving both session histories. If the Split Worker is still running, Clay interrupts it before closing the pair.",
|
|
46
|
-
inputSchema: buildShape({}),
|
|
48
|
+
inputSchema: buildShape({ workerId: { type: "number", description: "Optional exact Worker session id; required when multiple Workers are configured." } }),
|
|
47
49
|
handler: function (args) { return handlers.close(args || {}); },
|
|
48
50
|
},
|
|
49
51
|
];
|
|
@@ -55,13 +57,14 @@ function getToolDefs(handlers, options) {
|
|
|
55
57
|
inputSchema: buildShape({
|
|
56
58
|
message: { type: "string", description: "Bounded correction or context for the current active Worker turn." },
|
|
57
59
|
requestId: { type: "string", description: "Optional stable request id. Reuse it to deduplicate a retried delivery within this human turn." },
|
|
60
|
+
workerId: { type: "number", description: "Optional exact Worker session id; required when multiple Workers are configured." },
|
|
58
61
|
}, ["message"]),
|
|
59
62
|
handler: function (args) { return handlers.message(args || {}); },
|
|
60
63
|
},
|
|
61
64
|
{
|
|
62
65
|
name: "partner_status",
|
|
63
66
|
description: "Bounded capacity and continuity report for your Split Worker, for deciding reuse against replacement: context tokens used and the ratio of its window, current task and activity, vendor/model/effort, history size and idle time, whether it is safe to replace right now, and your own recorded results for earlier Worker generations. Returns no transcript. Prefer this over reading turns when you only need to decide.",
|
|
64
|
-
inputSchema: buildShape({}),
|
|
67
|
+
inputSchema: buildShape({ workerId: { type: "number", description: "Optional exact Worker session id; required when multiple Workers are configured." } }),
|
|
65
68
|
handler: function (args) { return handlers.status(args || {}); },
|
|
66
69
|
},
|
|
67
70
|
{
|
|
@@ -80,6 +83,7 @@ function getToolDefs(handlers, options) {
|
|
|
80
83
|
},
|
|
81
84
|
operationId: { type: "string", description: "Optional stable id for this replacement. Reusing it within the same human turn returns the original operation instead of replacing twice." },
|
|
82
85
|
supersedeProposalId: { type: "string", description: "Exact pending replacement proposal id to supersede with this new configuration." },
|
|
86
|
+
workerId: { type: "number", description: "Optional exact Worker session id; required when multiple Workers are configured." },
|
|
83
87
|
}, ["message", "recommendationRationale"]),
|
|
84
88
|
handler: function (args) { return handlers.replace(args || {}); },
|
|
85
89
|
},
|
|
@@ -90,6 +94,7 @@ function getToolDefs(handlers, options) {
|
|
|
90
94
|
outcome: { enum: EVALUATION_OUTCOMES, description: 'One of "succeeded", "partial", "failed", "abandoned".' },
|
|
91
95
|
note: { type: "string", description: "Optional short reason, up to 400 characters." },
|
|
92
96
|
generation: { type: "number", description: "Optional generation number. Defaults to the current Worker." },
|
|
97
|
+
workerId: { type: "number", description: "Optional exact Worker session id; required when multiple Workers are configured." },
|
|
93
98
|
}, ["outcome"]),
|
|
94
99
|
handler: function (args) { return handlers.evaluate(args || {}); },
|
|
95
100
|
},
|