clay-server 4.2.0-beta.3 → 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
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
// Split Worker lifecycle internals: bounded status, transactional replacement,
|
|
2
|
-
// and the per-generation evaluation ledger. The Driver-facing tool posts a
|
|
3
|
-
// user-controlled runtime card before accepted replacement reaches this module.
|
|
4
|
-
//
|
|
5
|
-
// Everything is bound to the exact live pair. The Driver is re-resolved from
|
|
6
|
-
// the split store on every call, its structural capability is re-checked every time, and
|
|
7
|
-
// ownership must match on both sessions. No active-tab or global-session
|
|
8
|
-
// fallback exists in this module.
|
|
9
|
-
//
|
|
10
|
-
// What replacement deliberately does NOT do: delete history. Dissolving a pair
|
|
11
|
-
// leaves both sessions in the project exactly as close_partner already does, so
|
|
12
|
-
// the previous Worker's conversation stays browsable and recoverable under the
|
|
13
|
-
// existing session semantics. There is no archive concept in the repo to hook
|
|
14
|
-
// into, and inventing a destructive one would lose work.
|
|
15
|
-
|
|
16
1
|
var eligibility = require("./session-driver-eligibility");
|
|
17
2
|
var replacementState = require("./project-pair-replacement-state");
|
|
18
3
|
var pairUsage = require("./project-pair-usage");
|
|
@@ -66,9 +51,6 @@ function continuityStatus(session) {
|
|
|
66
51
|
};
|
|
67
52
|
}
|
|
68
53
|
|
|
69
|
-
// The current task, as a bounded preview of the delegated instruction only.
|
|
70
|
-
// Never the transcript: a Driver deciding reuse needs to know what the Worker
|
|
71
|
-
// is on, not to re-read its conversation.
|
|
72
54
|
function activityStatus(session) {
|
|
73
55
|
var token = session._pairDelegation || null;
|
|
74
56
|
return {
|
|
@@ -185,10 +167,6 @@ function attachPairLifecycle(ctx) {
|
|
|
185
167
|
}
|
|
186
168
|
function resolveDriverPair(caller) {
|
|
187
169
|
if (!caller) throw new Error("pair lifecycle tools require a session-bound tool server");
|
|
188
|
-
// Exact live object identity, before anything is read off the caller. A
|
|
189
|
-
// tool handler captured by a query outlives the session it was bound to, so
|
|
190
|
-
// a stale object — or a different object that happens to carry the same
|
|
191
|
-
// localId — must not be able to act as the Driver.
|
|
192
170
|
if (sm.sessions.get(caller.localId) !== caller) {
|
|
193
171
|
throw new Error("this session is no longer live; the pair tools are bound to an exact session");
|
|
194
172
|
}
|
|
@@ -268,6 +246,38 @@ function attachPairLifecycle(ctx) {
|
|
|
268
246
|
};
|
|
269
247
|
}
|
|
270
248
|
|
|
249
|
+
function sourcePairRegistered(caller, worker) {
|
|
250
|
+
try {
|
|
251
|
+
if (sm.sessions.get(caller.localId) !== caller || sm.sessions.get(worker.localId) !== worker) return false;
|
|
252
|
+
if ((caller.ownerId || null) !== (worker.ownerId || null)) return false;
|
|
253
|
+
var group = store.groupForMember(caller.localId);
|
|
254
|
+
return !!(group && group.pair && group.pair.driverId === caller.localId &&
|
|
255
|
+
group.pair.workerId === worker.localId && group.members.indexOf(caller.localId) !== -1 &&
|
|
256
|
+
group.members.indexOf(worker.localId) !== -1);
|
|
257
|
+
} catch (e) { return false; }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function restoreSourcePair(ws, caller, worker) {
|
|
261
|
+
try {
|
|
262
|
+
return store.create(ws, {
|
|
263
|
+
members: [caller.localId, worker.localId],
|
|
264
|
+
pair: { driverId: caller.localId, workerId: worker.localId },
|
|
265
|
+
});
|
|
266
|
+
} catch (e) { return { ok: false, error: e.message || String(e) }; }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function replacementTargetRegistered(caller, oldWorker) {
|
|
270
|
+
try {
|
|
271
|
+
if (sm.sessions.get(caller.localId) !== caller || sm.sessions.get(oldWorker.localId) !== oldWorker) return null;
|
|
272
|
+
var group = store.groupForMember(caller.localId);
|
|
273
|
+
if (!group || !group.pair || group.pair.driverId !== caller.localId ||
|
|
274
|
+
group.pair.workerId === oldWorker.localId) return null;
|
|
275
|
+
var target = sm.sessions.get(group.pair.workerId);
|
|
276
|
+
if (!target || sm.sessions.get(target.localId) !== target) return null;
|
|
277
|
+
return (caller.ownerId || null) === (target.ownerId || null) ? target : null;
|
|
278
|
+
} catch (e) { return null; }
|
|
279
|
+
}
|
|
280
|
+
|
|
271
281
|
function replacePartner(args, caller) {
|
|
272
282
|
var resolved = resolveDriverPair(caller);
|
|
273
283
|
var group = resolved.group;
|
|
@@ -278,10 +288,6 @@ function attachPairLifecycle(ctx) {
|
|
|
278
288
|
var replay = replacementState.replayValue(replacementEntry);
|
|
279
289
|
if (replay) return Promise.resolve(replay);
|
|
280
290
|
|
|
281
|
-
// Validate the replacement while the old pair is still fully intact. An
|
|
282
|
-
// uninstalled vendor, an unavailable model or an unsupported effort must
|
|
283
|
-
// never cost the user their running Worker, so this precedes the interrupt,
|
|
284
|
-
// the permission cancellation and the dissolve.
|
|
285
291
|
try {
|
|
286
292
|
ctx.preflightWorkerForDriver(caller, {
|
|
287
293
|
workerVendor: args.workerVendor,
|
|
@@ -309,51 +315,39 @@ function attachPairLifecycle(ctx) {
|
|
|
309
315
|
throw validationError;
|
|
310
316
|
}
|
|
311
317
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
ctx.finishDelegation
|
|
318
|
+
var ws = { _clayUser: caller.ownerId ? { id: caller.ownerId } : null };
|
|
319
|
+
try {
|
|
320
|
+
if (blocked) {
|
|
321
|
+
replacementState.stage(replacementEntry.transaction, "stopping_source");
|
|
322
|
+
replacementEntry.transaction.sourceStopped = true;
|
|
323
|
+
if (ctx.markInterruption) ctx.markInterruption(oldWorker, "driver", "The Driver accepted replacement of this Worker generation.");
|
|
324
|
+
oldWorker.taskStopRequested = true;
|
|
325
|
+
if (oldWorker.abortController) {
|
|
326
|
+
try { oldWorker.abortController.abort(); } catch (e) {}
|
|
327
|
+
}
|
|
328
|
+
if (oldWorker._pairDelegation && typeof ctx.finishDelegation === "function") {
|
|
329
|
+
if (typeof ctx.completeInterruptedTask === "function") ctx.completeInterruptedTask(oldWorker, caller, oldWorker._pairDelegation);
|
|
330
|
+
ctx.finishDelegation(group, caller, oldWorker, oldWorker._pairDelegation);
|
|
331
|
+
}
|
|
323
332
|
}
|
|
324
|
-
}
|
|
325
333
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
334
|
+
replacementState.stage(replacementEntry.transaction, "cancelling_source_waits");
|
|
335
|
+
if (typeof ctx.cancelWorkerPermissions === "function") {
|
|
336
|
+
ctx.cancelWorkerPermissions(oldWorker, "The Driver replaced this Split Worker.");
|
|
337
|
+
}
|
|
331
338
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
339
|
+
replacementState.stage(replacementEntry.transaction, "dissolving_source_pair");
|
|
340
|
+
var dissolved = store.dissolve(ws, { id: group.id });
|
|
341
|
+
if (!dissolved.ok) throw new Error(dissolved.error || "could not dissolve the existing pair");
|
|
342
|
+
} catch (precreateError) {
|
|
343
|
+
var sourceIntact = sourcePairRegistered(caller, oldWorker);
|
|
344
|
+
var precreateRestored = sourceIntact ? { ok: true } : restoreSourcePair(ws, caller, oldWorker);
|
|
345
|
+
var rollback = precreateRestored && precreateRestored.ok ? (sourceIntact ? "safe_retry" : "source_pair_restored") : "source_pair_restore_failed";
|
|
336
346
|
turnControl.releaseCreation(creationTicket);
|
|
337
|
-
replacementState.fail(replacementEntry.transaction, "
|
|
338
|
-
throw
|
|
347
|
+
replacementState.fail(replacementEntry.transaction, "precreate_failed", precreateError, rollback);
|
|
348
|
+
throw precreateError;
|
|
339
349
|
}
|
|
340
350
|
|
|
341
|
-
// History is preserved: the old Worker session stays in the project.
|
|
342
|
-
//
|
|
343
|
-
// Preflight already cleared every input, but the group write itself can
|
|
344
|
-
// still fail, so the dissolve is rolled back rather than leaving the Driver
|
|
345
|
-
// with no pair at all. The restored group is an equivalent record with the
|
|
346
|
-
// same members and roles; its group id is newly issued.
|
|
347
|
-
//
|
|
348
|
-
// What rollback can and cannot undo:
|
|
349
|
-
// - Idle replacement failure is fully recoverable. The Worker session,
|
|
350
|
-
// its history and its still-open ledger generation are all preserved,
|
|
351
|
-
// and the pair is restored.
|
|
352
|
-
// - An explicit interrupt=true is NOT reversible. Stopping a mid-turn
|
|
353
|
-
// Worker aborts its query and cancels the permission decisions it was
|
|
354
|
-
// waiting on; a later creation failure cannot resume that turn. The
|
|
355
|
-
// session and its history survive, but the interrupted work does not
|
|
356
|
-
// come back. The error says so rather than implying a clean restore.
|
|
357
351
|
var created;
|
|
358
352
|
try {
|
|
359
353
|
replacementState.stage(replacementEntry.transaction, "creating_target");
|
|
@@ -363,18 +357,26 @@ function attachPairLifecycle(ctx) {
|
|
|
363
357
|
workerEffort: typeof args.workerEffort === "string" ? args.workerEffort : "",
|
|
364
358
|
});
|
|
365
359
|
} catch (e) {
|
|
366
|
-
var
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
360
|
+
var target = replacementTargetRegistered(caller, oldWorker);
|
|
361
|
+
if (target) {
|
|
362
|
+
var targetClosed = closeGeneration(caller, oldWorker);
|
|
363
|
+
var targetGeneration = recordGenerationStart(caller, target);
|
|
364
|
+
replacementEntry.transaction.targetWorkerId = target.localId;
|
|
365
|
+
replacementEntry.transaction.targetGeneration = targetGeneration;
|
|
366
|
+
replacementEntry.transaction.previousGeneration = targetClosed ? targetClosed.generation : null;
|
|
367
|
+
replacementState.fail(replacementEntry.transaction, "postcommit_delivery_failed", e, "target_created");
|
|
368
|
+
throw new Error("the replacement Split Worker was created, but its delivery confirmation failed. Reuse Worker " + target.localId + " generation " + targetGeneration + " with send_to_partner; do not replace it again. " + (e.message || String(e)));
|
|
369
|
+
}
|
|
370
|
+
var restored = restoreSourcePair(ws, caller, oldWorker);
|
|
371
|
+
var restoredLive = restored && restored.ok && sourcePairRegistered(caller, oldWorker);
|
|
372
|
+
var restoreNote = restoredLive
|
|
371
373
|
? "The previous pair was restored with its session, history and open generation intact."
|
|
372
374
|
: "The previous pair could not be restored; both sessions are intact and unpaired.";
|
|
373
375
|
var interruptNote = blocked
|
|
374
376
|
? " Its interrupted turn cannot be resumed, because stopping it was explicitly requested."
|
|
375
377
|
: "";
|
|
376
378
|
turnControl.releaseCreation(creationTicket);
|
|
377
|
-
replacementState.fail(replacementEntry.transaction, "creation_failed", e,
|
|
379
|
+
replacementState.fail(replacementEntry.transaction, "creation_failed", e, restoredLive ? "source_pair_restored" : "source_pair_restore_failed");
|
|
378
380
|
throw new Error("could not create the replacement Split Worker: " + (e.message || String(e)) +
|
|
379
381
|
". " + restoreNote + interruptNote);
|
|
380
382
|
}
|
|
@@ -384,7 +386,6 @@ function attachPairLifecycle(ctx) {
|
|
|
384
386
|
if (typeof sm.saveSessionFile === "function") sm.saveSessionFile(caller);
|
|
385
387
|
}
|
|
386
388
|
var generation = recordGenerationStart(caller, created.worker);
|
|
387
|
-
|
|
388
389
|
var result = {
|
|
389
390
|
status: "replaced",
|
|
390
391
|
previousWorkerSessionId: oldWorker.localId,
|
|
@@ -410,7 +411,6 @@ function attachPairLifecycle(ctx) {
|
|
|
410
411
|
});
|
|
411
412
|
}
|
|
412
413
|
|
|
413
|
-
// No global or cross-user ranking is formed from Worker evaluations.
|
|
414
414
|
function validateEvaluation(raw) {
|
|
415
415
|
var input = typeof raw === "string" ? { outcome: raw } : (raw && typeof raw === "object" ? raw : {});
|
|
416
416
|
var outcome = typeof input.outcome === "string" ? input.outcome.trim().toLowerCase() : "";
|
|
@@ -46,7 +46,11 @@ function fail(transaction, stageName, error, rollback) {
|
|
|
46
46
|
transaction.stage = stageName;
|
|
47
47
|
transaction.updatedAt = Date.now();
|
|
48
48
|
transaction.completedAt = transaction.updatedAt;
|
|
49
|
-
transaction.failure = {
|
|
49
|
+
transaction.failure = {
|
|
50
|
+
message: error && (error.message || String(error)) || "Unknown replacement failure",
|
|
51
|
+
rollback: rollback || "not_required",
|
|
52
|
+
retryable: rollback === "safe_retry" || (stageName === "creation_failed" && rollback === "source_pair_restored"),
|
|
53
|
+
};
|
|
50
54
|
transaction.sourcePairRestored = rollback === "source_pair_restored" ? true : (rollback === "source_pair_restore_failed" ? false : null);
|
|
51
55
|
return transaction;
|
|
52
56
|
}
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
var yoke = require("./yoke");
|
|
2
2
|
var contextBuilder = require("./session-handoff-context");
|
|
3
3
|
var sessionHandoffMcp = require("./session-handoff-mcp-server");
|
|
4
|
+
var discoveryModule = require("./session-handoff-discovery");
|
|
4
5
|
|
|
5
6
|
var MAX_HANDOFF_CHAIN_DEPTH = 5;
|
|
6
7
|
|
|
7
|
-
function toolResult(text) {
|
|
8
|
-
return Promise.resolve({ content: [{ type: "text", text: text }] });
|
|
9
|
-
}
|
|
10
|
-
|
|
11
8
|
function toolError(message) {
|
|
12
9
|
return Promise.resolve({
|
|
13
10
|
content: [{ type: "text", text: "Error: " + message }],
|
|
@@ -26,6 +23,16 @@ function hasUserContext(session) {
|
|
|
26
23
|
|
|
27
24
|
function attachSessionHandoff(ctx) {
|
|
28
25
|
var sm = ctx.sm;
|
|
26
|
+
var discovery = discoveryModule.attachSessionHandoffDiscovery({
|
|
27
|
+
sm: sm,
|
|
28
|
+
isMate: ctx.isMate,
|
|
29
|
+
projectSlug: ctx.projectSlug || "current-project",
|
|
30
|
+
isMultiUser: ctx.isMultiUser,
|
|
31
|
+
getProjectAccess: ctx.getProjectAccess,
|
|
32
|
+
canAccessSession: ctx.canAccessSession,
|
|
33
|
+
findUserById: ctx.findUserById,
|
|
34
|
+
isDriverOperatedSession: ctx.isDriverOperatedSession,
|
|
35
|
+
});
|
|
29
36
|
|
|
30
37
|
function sendResult(ws, ok, details) {
|
|
31
38
|
ctx.sendTo(ws, Object.assign({ type: "session_handoff_result", ok: ok }, details || {}));
|
|
@@ -172,14 +179,16 @@ function attachSessionHandoff(ctx) {
|
|
|
172
179
|
var chain = [];
|
|
173
180
|
var visited = new Set();
|
|
174
181
|
var sourceSessionId = boundSession.handoff.sourceSessionId;
|
|
175
|
-
var ownerId = boundSession.ownerId || null;
|
|
176
182
|
for (var depth = 0; depth < MAX_HANDOFF_CHAIN_DEPTH; depth++) {
|
|
177
183
|
var visitKey = String(sourceSessionId);
|
|
178
184
|
if (visited.has(visitKey)) break;
|
|
179
185
|
visited.add(visitKey);
|
|
180
186
|
var source = sm.sessions.get(sourceSessionId);
|
|
181
187
|
if (!source) return { error: "Source session was not found: " + sourceSessionId };
|
|
182
|
-
if ((source
|
|
188
|
+
if (!discovery.authorize(boundSession, source)) {
|
|
189
|
+
if (typeof ctx.isMultiUser !== "function" || !ctx.isMultiUser()) return { error: "Source session owner does not match this session" };
|
|
190
|
+
return { error: "Source session access denied" };
|
|
191
|
+
}
|
|
183
192
|
chain.push(source);
|
|
184
193
|
if (!source.handoff || source.handoff.sourceSessionId === undefined || source.handoff.sourceSessionId === null) break;
|
|
185
194
|
sourceSessionId = source.handoff.sourceSessionId;
|
|
@@ -195,61 +204,45 @@ function attachSessionHandoff(ctx) {
|
|
|
195
204
|
return null;
|
|
196
205
|
}
|
|
197
206
|
|
|
198
|
-
function
|
|
199
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
200
|
-
return Math.min(max, Math.max(min, Math.floor(value)));
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function formatHistory(source, args) {
|
|
204
|
-
var history = Array.isArray(source.history) ? source.history : [];
|
|
205
|
-
var limit = boundedInteger(args.limit, 30, 1, 100);
|
|
206
|
-
var offset = typeof args.offset === "number" && Number.isFinite(args.offset)
|
|
207
|
-
? boundedInteger(args.offset, 0, 0, history.length)
|
|
208
|
-
: Math.max(0, history.length - limit);
|
|
209
|
-
var slice = history.slice(offset, offset + limit);
|
|
210
|
-
var end = offset + slice.length;
|
|
211
|
-
var out = [];
|
|
212
|
-
out.push("# " + (source.title || "Untitled session") + " — " + (source.vendor || "unknown") + "/" + source.localId);
|
|
213
|
-
out.push("Showing entries " + (slice.length ? offset + 1 : 0) + "-" + end + " of " + history.length + "\n");
|
|
214
|
-
for (var i = 0; i < slice.length; i++) {
|
|
215
|
-
var entry = slice[i];
|
|
216
|
-
if (!entry) continue;
|
|
217
|
-
var label;
|
|
218
|
-
var text = "";
|
|
219
|
-
if (entry.type === "user_message") {
|
|
220
|
-
label = "USER";
|
|
221
|
-
text = entry.text || "";
|
|
222
|
-
} else if (entry.type === "delta") {
|
|
223
|
-
label = "ASSISTANT";
|
|
224
|
-
text = entry.text || "";
|
|
225
|
-
} else if (entry.type === "tool_executing" || entry.type === "tool_result") {
|
|
226
|
-
label = "TOOL";
|
|
227
|
-
text = (entry.name || "") + (entry.input ? " " + JSON.stringify(entry.input).substring(0, 120) : "");
|
|
228
|
-
} else {
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
if (text.length > 800) text = text.substring(0, 800) + "...";
|
|
232
|
-
out.push("[" + label + "] " + text);
|
|
233
|
-
}
|
|
234
|
-
return out.join("\n");
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function readHandoffSource(args, boundSession) {
|
|
207
|
+
function readHandoffSource(args, boundSession, generation) {
|
|
238
208
|
if (!boundSession) return toolError("read_handoff_source requires a session-bound tool server");
|
|
239
|
-
if (!boundSession.handoff) return
|
|
209
|
+
if (!boundSession.handoff) return Promise.resolve(discovery.fail(boundSession, generation, "this session does not have a handoff source"));
|
|
240
210
|
var resolved = resolveSourceChain(boundSession);
|
|
241
|
-
if (resolved.error) return
|
|
211
|
+
if (resolved.error) return Promise.resolve(discovery.fail(boundSession, generation, resolved.error));
|
|
242
212
|
var source = findSource(resolved.chain, args.sourceSessionId);
|
|
243
|
-
if (!source) return
|
|
244
|
-
return
|
|
213
|
+
if (!source) return Promise.resolve(discovery.fail(boundSession, generation, "sourceSessionId is not in this session's handoff chain"));
|
|
214
|
+
return Promise.resolve(discovery.legacy(boundSession, source, args, generation));
|
|
245
215
|
}
|
|
246
216
|
|
|
247
|
-
function getToolDefs(boundSession) {
|
|
217
|
+
function getToolDefs(boundSession, queryGeneration) {
|
|
248
218
|
if (ctx.isMate) return [];
|
|
249
|
-
if (boundSession && !boundSession
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
219
|
+
if (boundSession && !discovery.authorize(boundSession, boundSession)) return [];
|
|
220
|
+
var generation = boundSession
|
|
221
|
+
? (Number.isInteger(queryGeneration) ? queryGeneration : discovery.captureGeneration(boundSession))
|
|
222
|
+
: null;
|
|
223
|
+
var handlers = {
|
|
224
|
+
read: function (args) {
|
|
225
|
+
if (!boundSession) return toolError("read_handoff_source requires a session-bound tool server");
|
|
226
|
+
if (!boundSession.handoff) return toolError("read_handoff_source requires a handoff session");
|
|
227
|
+
return readHandoffSource(args, boundSession, generation);
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
if (boundSession && !boundSession.handoff) delete handlers.read;
|
|
231
|
+
if (boundSession) {
|
|
232
|
+
handlers.listOtherDrivers = function (args) {
|
|
233
|
+
try { return Promise.resolve(discovery.list(boundSession, args || {}, generation)); }
|
|
234
|
+
catch (e) { return toolError(e.message || String(e)); }
|
|
235
|
+
};
|
|
236
|
+
handlers.searchOtherDrivers = function (args) {
|
|
237
|
+
try { return Promise.resolve(discovery.search(boundSession, args || {}, generation)); }
|
|
238
|
+
catch (e) { return toolError(e.message || String(e)); }
|
|
239
|
+
};
|
|
240
|
+
handlers.readOtherDriver = function (args) {
|
|
241
|
+
try { return Promise.resolve(discovery.read(boundSession, args || {}, generation)); }
|
|
242
|
+
catch (e) { return toolError(e.message || String(e)); }
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return sessionHandoffMcp.getToolDefs(handlers);
|
|
253
246
|
}
|
|
254
247
|
|
|
255
248
|
function createMcpServer(adapter, boundSession) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
var path = require("path");
|
|
2
2
|
var fs = require("fs");
|
|
3
|
+
var continuationLease = require("./driver-continuation-lease");
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Attach user-message handler and remaining small handlers
|
|
@@ -433,6 +434,19 @@ function attachUserMessage(ctx) {
|
|
|
433
434
|
finishPendingEarly(false, "Pending messages cannot be delivered to a Driver-operated Split Worker");
|
|
434
435
|
return true;
|
|
435
436
|
}
|
|
437
|
+
if (!pendingInternal && session._driverContinuationLease && pendingMessageQueue) {
|
|
438
|
+
var continuationQueued = pendingMessageQueue.admit(session, msg, ws && ws._clayUser);
|
|
439
|
+
if (continuationQueued.ok) {
|
|
440
|
+
messageDelivery.acknowledge(ws, deliveryReceipt, null, session);
|
|
441
|
+
sendTo(ws, { type: "message_queued", clientMessageId: msg.clientMessageId || null,
|
|
442
|
+
sessionId: session.localId, projectSlug: slug, state: "pending",
|
|
443
|
+
paused: pendingMessageQueue.isPaused(session), revision: pendingMessageQueue.getRevision() });
|
|
444
|
+
continuationLease.cancelForHumanMessage(session);
|
|
445
|
+
} else {
|
|
446
|
+
sendTo(ws, { type: "error", text: continuationQueued.error || "Unable to queue message" });
|
|
447
|
+
}
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
436
450
|
if (!pendingInternal && pendingMessageQueue && (pendingMessageQueue.hasActive(session) || session.isProcessing || session._queryStarting || session._awaitingTurnResult)) {
|
|
437
451
|
var queuedResult = pendingMessageQueue.admit(session, msg, ws && ws._clayUser);
|
|
438
452
|
if (queuedResult.ok) {
|
|
@@ -339,7 +339,7 @@ function attachWorkerProposal(ctx) {
|
|
|
339
339
|
} else if (result.status === "running") {
|
|
340
340
|
followup = "[Split Worker execution is still running]\nUse read_partner to inspect progress before completing the task.";
|
|
341
341
|
} else {
|
|
342
|
-
followup = "[Split Worker execution failed]\
|
|
342
|
+
followup = "[Split Worker execution failed]\nThe existing Split Worker may still be reusable. Inspect partner_status and send a narrower follow-up with send_to_partner when the Worker and pair still exist; do not replace it merely because its task or transport failed. Retry replacement only when the replacement transaction reports that no new Worker was created and the failure is explicitly safe to retry.\n\n" + (result.error || result.response || "Unknown Split Worker error.");
|
|
343
343
|
}
|
|
344
344
|
await resumeDriver(session, followup);
|
|
345
345
|
}
|
package/lib/project.js
CHANGED
|
@@ -56,6 +56,8 @@ var { attachCapsuleTurn } = require("./project-capsule-turn");
|
|
|
56
56
|
var workspaceSessionRef = require("./workspace-query-service").sessionRef;
|
|
57
57
|
var { attachSessionPair } = require("./project-session-pair");
|
|
58
58
|
var { attachSessionHandoff } = require("./project-session-handoff");
|
|
59
|
+
var { attachDriverContinuation } = require("./project-driver-continuation");
|
|
60
|
+
var driverContinuationAccess = require("./driver-continuation-access");
|
|
59
61
|
var { attachSessionNotes, composeSystemPrompts } = require("./project-session-notes");
|
|
60
62
|
var { attachSessionDocument } = require("./project-session-document");
|
|
61
63
|
var { attachCapsuleCatalog } = require("./project-capsule-catalog");
|
|
@@ -854,16 +856,41 @@ function createProjectContext(opts) {
|
|
|
854
856
|
});
|
|
855
857
|
var _sessionHandoff = attachSessionHandoff({
|
|
856
858
|
cwd: cwd,
|
|
859
|
+
projectSlug: slug,
|
|
857
860
|
sm: sm,
|
|
858
861
|
isMate: isMate,
|
|
859
862
|
splitStore: _splitGroups.store,
|
|
860
863
|
getSdk: function () { return sdk; },
|
|
861
864
|
sendTo: sendTo,
|
|
862
865
|
usersModule: usersModule,
|
|
866
|
+
isMultiUser: usersModule.isMultiUser,
|
|
867
|
+
getProjectAccess: opts.getProjectAccess || function () { return { visibility: "public", ownerId: projectOwnerId || null }; },
|
|
868
|
+
canAccessSession: usersModule.canAccessSession,
|
|
869
|
+
findUserById: usersModule.findUserById,
|
|
870
|
+
isDriverOperatedSession: function (session) { return !isMate && _sessionPair.workerPermission.isDriverOperated(session); },
|
|
863
871
|
adapters: adapters,
|
|
864
872
|
getLinuxUserForSession: getLinuxUserForSession,
|
|
865
873
|
onProcessingChanged: onProcessingChanged,
|
|
866
874
|
});
|
|
875
|
+
var _driverContinuation = attachDriverContinuation({
|
|
876
|
+
sm: sm,
|
|
877
|
+
isMate: isMate,
|
|
878
|
+
projectSlug: slug,
|
|
879
|
+
splitStore: _splitGroups.store,
|
|
880
|
+
pendingMessageQueue: _pendingMessageQueue,
|
|
881
|
+
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
882
|
+
isMultiUser: usersModule.isMultiUser,
|
|
883
|
+
isDriverOperatedSession: function (session) { return !isMate && _sessionPair.workerPermission.isDriverOperated(session); },
|
|
884
|
+
authorizeSession: function (session, ownerId) {
|
|
885
|
+
return driverContinuationAccess.authorize({ usersModule: usersModule, projectSlug: slug,
|
|
886
|
+
getProjectAccess: opts.getProjectAccess, canAccessProjectSlug: opts.canAccessProjectSlug }, session, ownerId);
|
|
887
|
+
},
|
|
888
|
+
getSdk: function () { return sdk; },
|
|
889
|
+
getLinuxUserForSession: getLinuxUserForSession,
|
|
890
|
+
onProcessingChanged: onProcessingChanged,
|
|
891
|
+
consumePendingMessage: function (session) { return _userMessage && _userMessage.consumePendingMessage(session); },
|
|
892
|
+
sendTo: sendTo,
|
|
893
|
+
});
|
|
867
894
|
var _debate = null;
|
|
868
895
|
var _debateProposal = attachDebateProposal({
|
|
869
896
|
cwd: cwd,
|
|
@@ -1287,6 +1314,7 @@ function createProjectContext(opts) {
|
|
|
1287
1314
|
_sessionPair.getSystemPrompt(session),
|
|
1288
1315
|
_sessionNotes.getSystemPrompt(session),
|
|
1289
1316
|
_sessionHandoff.getSystemPrompt(session),
|
|
1317
|
+
_driverContinuation.getSystemPrompt(session),
|
|
1290
1318
|
_sessionDocument.getSystemPrompt(session),
|
|
1291
1319
|
_capsuleCatalog.getSystemPrompt(session),
|
|
1292
1320
|
_projectLogs.getSystemPrompt(session),
|
|
@@ -1304,6 +1332,7 @@ function createProjectContext(opts) {
|
|
|
1304
1332
|
return _sessionPair.getToolDefs(session)
|
|
1305
1333
|
.concat(_sessionNotes.getToolDefs(session))
|
|
1306
1334
|
.concat(_sessionHandoff.getToolDefs(session))
|
|
1335
|
+
.concat(_driverContinuation.getToolDefs(session, Number(session._sdkQueryGeneration || 0)))
|
|
1307
1336
|
.concat(_sessionDocument.getToolDefs(session))
|
|
1308
1337
|
.concat(_debateProposal.getToolDefs(session))
|
|
1309
1338
|
.concat(_mateCreationProposal.getToolDefs(session))
|
|
@@ -1714,6 +1743,7 @@ function createProjectContext(opts) {
|
|
|
1714
1743
|
// --- Sessions, config, project mgmt (delegated to project-sessions.js) ---
|
|
1715
1744
|
if (_sessionPair.handleMessage(ws, msg)) return;
|
|
1716
1745
|
if (_sessionHandoff.handleMessage(ws, msg)) return;
|
|
1746
|
+
if (_driverContinuation.handleMessage(ws, msg)) return;
|
|
1717
1747
|
if (_splitGroups.handleMessage(ws, msg)) return;
|
|
1718
1748
|
if (_models.handleMessage(ws, msg)) return;
|
|
1719
1749
|
if (_scheduledTasks.handleMessage(ws, msg)) return;
|
|
@@ -2226,7 +2256,7 @@ function createProjectContext(opts) {
|
|
|
2226
2256
|
inputSchema: normalizeToolSchema(noteTools[nti].inputSchema),
|
|
2227
2257
|
});
|
|
2228
2258
|
}
|
|
2229
|
-
var handoffTools = _sessionHandoff.getToolDefs(boundSession);
|
|
2259
|
+
var handoffTools = _sessionHandoff.getToolDefs(boundSession, queryGeneration);
|
|
2230
2260
|
for (var hti = 0; hti < handoffTools.length; hti++) {
|
|
2231
2261
|
tools.push({
|
|
2232
2262
|
server: "clay-handoff",
|
|
@@ -2235,6 +2265,15 @@ function createProjectContext(opts) {
|
|
|
2235
2265
|
inputSchema: normalizeToolSchema(handoffTools[hti].inputSchema),
|
|
2236
2266
|
});
|
|
2237
2267
|
}
|
|
2268
|
+
var continuationTools = _driverContinuation.getToolDefs(boundSession, queryGeneration);
|
|
2269
|
+
for (var cti = 0; cti < continuationTools.length; cti++) {
|
|
2270
|
+
tools.push({
|
|
2271
|
+
server: "clay-continuation",
|
|
2272
|
+
name: continuationTools[cti].name,
|
|
2273
|
+
description: continuationTools[cti].description || continuationTools[cti].name,
|
|
2274
|
+
inputSchema: normalizeToolSchema(continuationTools[cti].inputSchema),
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2238
2277
|
var documentTools = _sessionDocument.getToolDefs(boundSession);
|
|
2239
2278
|
for (var dti = 0; dti < documentTools.length; dti++) {
|
|
2240
2279
|
tools.push({
|
|
@@ -2321,13 +2360,21 @@ function createProjectContext(opts) {
|
|
|
2321
2360
|
}
|
|
2322
2361
|
}
|
|
2323
2362
|
if (boundSession && serverName === "clay-handoff") {
|
|
2324
|
-
var handoffTools = _sessionHandoff.getToolDefs(boundSession);
|
|
2363
|
+
var handoffTools = _sessionHandoff.getToolDefs(boundSession, queryGeneration);
|
|
2325
2364
|
for (var hti = 0; hti < handoffTools.length; hti++) {
|
|
2326
2365
|
if (handoffTools[hti].name === toolName && typeof handoffTools[hti].handler === "function") {
|
|
2327
2366
|
return Promise.resolve(handoffTools[hti].handler(args || {}));
|
|
2328
2367
|
}
|
|
2329
2368
|
}
|
|
2330
2369
|
}
|
|
2370
|
+
if (boundSession && serverName === "clay-continuation") {
|
|
2371
|
+
var continuationTools = _driverContinuation.getToolDefs(boundSession, queryGeneration);
|
|
2372
|
+
for (var cti = 0; cti < continuationTools.length; cti++) {
|
|
2373
|
+
if (continuationTools[cti].name === toolName && typeof continuationTools[cti].handler === "function") {
|
|
2374
|
+
return Promise.resolve(continuationTools[cti].handler(args || {}));
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2331
2378
|
if (boundSession && serverName === "clay-documents") {
|
|
2332
2379
|
var documentTools = _sessionDocument.getToolDefs(boundSession);
|
|
2333
2380
|
for (var dti = 0; dti < documentTools.length; dti++) {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
.driver-continuation-card,
|
|
2
|
+
.driver-continuation-context {
|
|
3
|
+
width: min(680px, calc(100% - 24px));
|
|
4
|
+
margin: 14px auto;
|
|
5
|
+
border: 1px solid var(--border);
|
|
6
|
+
border-radius: 14px;
|
|
7
|
+
background: var(--bg-alt);
|
|
8
|
+
color: var(--text);
|
|
9
|
+
box-shadow: 0 10px 28px rgba(0, 0, 0, .08);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.driver-continuation-card { padding: 18px; }
|
|
13
|
+
.driver-continuation-card header,
|
|
14
|
+
.driver-continuation-context { display: flex; align-items: flex-start; gap: 12px; }
|
|
15
|
+
.driver-continuation-context { padding: 15px 17px; }
|
|
16
|
+
.driver-continuation-card header > div,
|
|
17
|
+
.driver-continuation-context-copy { display: grid; gap: 3px; min-width: 0; flex: 1; }
|
|
18
|
+
.driver-continuation-mark { display: grid; place-items: center; width: 30px; height: 30px; flex: 0 0 auto; border-radius: 9px; color: var(--accent2); background: color-mix(in srgb, var(--accent2) 12%, transparent); }
|
|
19
|
+
.driver-continuation-kicker { font-size: 10px; line-height: 1.2; font-weight: 700; letter-spacing: .1em; color: var(--text-muted); }
|
|
20
|
+
.driver-continuation-status { margin-left: auto; white-space: nowrap; font-size: 12px; color: var(--text-muted); }
|
|
21
|
+
.driver-continuation-reason,
|
|
22
|
+
.driver-continuation-context p { margin: 12px 0; color: var(--text-secondary); line-height: 1.5; }
|
|
23
|
+
.driver-continuation-summary { display: grid; grid-template-columns: 80px 1fr; gap: 7px 12px; padding: 13px; border-radius: 10px; background: var(--bg); }
|
|
24
|
+
.driver-continuation-summary dt,
|
|
25
|
+
.driver-continuation-details dt { font-size: 11px; font-weight: 700; color: var(--text-muted); }
|
|
26
|
+
.driver-continuation-summary dd,
|
|
27
|
+
.driver-continuation-details dd { margin: 0; white-space: pre-wrap; line-height: 1.45; }
|
|
28
|
+
.driver-continuation-details { margin-top: 12px; color: var(--text-secondary); }
|
|
29
|
+
.driver-continuation-details summary { cursor: pointer; font-size: 12px; font-weight: 600; }
|
|
30
|
+
.driver-continuation-details dl { display: grid; grid-template-columns: 120px 1fr; gap: 8px 12px; margin: 12px 0 0; }
|
|
31
|
+
.driver-continuation-error { padding: 10px; border-radius: 8px; color: var(--error); background: var(--error-8); }
|
|
32
|
+
.driver-continuation-context .driver-continuation-error { color: var(--error); }
|
|
33
|
+
.driver-continuation-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
|
|
34
|
+
.driver-continuation-action,
|
|
35
|
+
.driver-continuation-source { min-height: 36px; border: 1px solid var(--border); border-radius: 9px; padding: 8px 13px; font: inherit; cursor: pointer; }
|
|
36
|
+
.driver-continuation-action.primary { display: inline-flex; align-items: center; gap: 7px; border-color: var(--accent2); background: color-mix(in srgb, var(--accent2) 16%, var(--bg)); color: var(--accent2); }
|
|
37
|
+
.driver-continuation-action.secondary,
|
|
38
|
+
.driver-continuation-source { background: transparent; color: var(--text); }
|
|
39
|
+
.driver-continuation-action:disabled,
|
|
40
|
+
.driver-continuation-source:disabled { cursor: not-allowed; opacity: .45; }
|
|
41
|
+
.driver-continuation-action:focus-visible,
|
|
42
|
+
.driver-continuation-source:focus-visible,
|
|
43
|
+
.driver-continuation-details summary:focus-visible { outline: 2px solid var(--accent2); outline-offset: 2px; }
|
|
44
|
+
.driver-continuation-source { justify-self: start; margin-top: 5px; padding: 5px 9px; min-height: 30px; font-size: 12px; }
|
|
45
|
+
|
|
46
|
+
@media (max-width: 600px) {
|
|
47
|
+
.driver-continuation-card,
|
|
48
|
+
.driver-continuation-context { width: calc(100% - 16px); }
|
|
49
|
+
.driver-continuation-card { padding: 14px; }
|
|
50
|
+
.driver-continuation-card header { flex-wrap: wrap; }
|
|
51
|
+
.driver-continuation-status { width: 100%; margin-left: 42px; }
|
|
52
|
+
.driver-continuation-summary,
|
|
53
|
+
.driver-continuation-details dl { grid-template-columns: 1fr; }
|
|
54
|
+
.driver-continuation-actions { flex-direction: column-reverse; }
|
|
55
|
+
.driver-continuation-action { width: 100%; justify-content: center; }
|
|
56
|
+
}
|