clay-server 4.2.0 → 4.3.0-beta.1
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-driver-continuation.js +499 -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 +14 -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-messages.js +14 -0
- package/lib/public/modules/driver-continuation-state.js +76 -0
- package/lib/public/modules/driver-continuation.js +260 -0
- package/lib/public/modules/permission-control.js +8 -3
- 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 +20 -2
- package/lib/ws-schema.js +6 -0
- package/package.json +1 -1
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,
|
|
@@ -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 = {
|
|
@@ -55,6 +55,11 @@ var DRIVER_CORE = [
|
|
|
55
55
|
"interrupt true, which stops it first only after acceptance. A human Stop",
|
|
56
56
|
"is authoritative: do not retry, send more work, or replace the Worker in the same turn. Clay blocks those",
|
|
57
57
|
"actions until the human sends a new Driver message. Use close_partner when they ask to close the pane.",
|
|
58
|
+
"If replacement fails before a new Worker is created, retry only when the transaction explicitly confirms a safe",
|
|
59
|
+
"rollback; still respect the failed-attempt cap and the human Stop barrier. If a new Worker was created but its task or transport",
|
|
60
|
+
"failed, keep that Worker and use send_to_partner for a follow-up; do not replace it again merely because execution",
|
|
61
|
+
"failed. True replacement quota exhaustion remains bounded and honest, and a human Stop always requires a new",
|
|
62
|
+
"human message.",
|
|
58
63
|
"A Worker outcome envelope is Worker-reported evidence, not Driver verification. Preserve partial results and",
|
|
59
64
|
"correlate completions to their task and generation; never attach a late old-generation result to a new Worker.",
|
|
60
65
|
"",
|