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.
Files changed (34) hide show
  1. package/lib/driver-continuation-access.js +54 -0
  2. package/lib/driver-continuation-lease.js +52 -0
  3. package/lib/driver-continuation-pair-transfer.js +86 -0
  4. package/lib/driver-continuation-pair.js +82 -0
  5. package/lib/driver-continuation-record.js +40 -0
  6. package/lib/driver-continuation-startup.js +68 -0
  7. package/lib/driver-continuation-transaction.js +61 -0
  8. package/lib/driver-continuation-trigger.js +110 -0
  9. package/lib/project-driver-continuation.js +499 -0
  10. package/lib/project-pair-lifecycle.js +73 -73
  11. package/lib/project-pair-replacement-state.js +5 -1
  12. package/lib/project-session-handoff.js +48 -55
  13. package/lib/project-user-message.js +14 -0
  14. package/lib/project-worker-proposal.js +1 -1
  15. package/lib/project.js +49 -2
  16. package/lib/public/css/driver-continuation.css +56 -0
  17. package/lib/public/css/overlays.css +44 -16
  18. package/lib/public/css/pane.css +9 -5
  19. package/lib/public/index.html +3 -3
  20. package/lib/public/modules/app-messages.js +14 -0
  21. package/lib/public/modules/driver-continuation-state.js +76 -0
  22. package/lib/public/modules/driver-continuation.js +260 -0
  23. package/lib/public/modules/permission-control.js +8 -3
  24. package/lib/public/style.css +1 -0
  25. package/lib/sdk-bridge.js +77 -0
  26. package/lib/session-handoff-discovery.js +443 -0
  27. package/lib/session-handoff-mcp-server.js +32 -4
  28. package/lib/session-pair-prompts.js +5 -0
  29. package/lib/session-pair-turn-control.js +19 -3
  30. package/lib/session-split-group-anchors.js +44 -0
  31. package/lib/session-split-groups.js +209 -59
  32. package/lib/sessions.js +20 -2
  33. package/lib/ws-schema.js +6 -0
  34. package/package.json +1 -1
@@ -0,0 +1,499 @@
1
+ var crypto = require("crypto");
2
+ var buildShape = require("./session-spawn-mcp-server").buildShape;
3
+ var sessionProvenance = require("./session-provenance");
4
+ var workspaceSessionRef = require("./workspace-query-service").sessionRef;
5
+ var workerProposalControl = require("./worker-proposal-control");
6
+ var transaction = require("./driver-continuation-transaction");
7
+ var continuationAccess = require("./driver-continuation-access");
8
+ var continuationLease = require("./driver-continuation-lease");
9
+ var continuationStartup = require("./driver-continuation-startup");
10
+ var continuationTrigger = require("./driver-continuation-trigger");
11
+ var continuationPair = require("./driver-continuation-pair");
12
+ var attachContinuationPairTransfer = require("./driver-continuation-pair-transfer").attachContinuationPairTransfer;
13
+ var continuationRecord = require("./driver-continuation-record");
14
+ var contextKey = continuationRecord.contextKey, findProposal = continuationRecord.findProposal,
15
+ hasEntries = continuationRecord.hasEntries, text = continuationRecord.text, toolResult = continuationRecord.toolResult;
16
+ var MAX_FIELD_CHARS = 2400;
17
+ var MAX_TOTAL_CHARS = 14000;
18
+ var ACTIVE_RUN_STATES = ["armed", "running", "reviewing", "waiting-worker", "waiting-user", "paused"];
19
+ var HANDOFF_FIELDS = ["goal", "constraints", "decisions", "rejectedApproaches", "unresolved", "nextAction", "verification", "repositoryState"];
20
+ function attachDriverContinuation(ctx) {
21
+ var sm = ctx.sm;
22
+ function isLiveDriver(session) {
23
+ return continuationAccess.isLiveDriver(session, ctx.isMate, sm.sessions, ctx.isDriverOperatedSession);
24
+ }
25
+
26
+ function skipPermissions(session) {
27
+ return !!(ctx.dangerouslySkipPermissions || session && (
28
+ session.dangerouslySkipPermissions || session.permissionMode === "bypassPermissions" ||
29
+ session.effectivePermissionMode === "bypassPermissions"
30
+ ));
31
+ }
32
+ function authorize(session, ownerId) {
33
+ if (!isLiveDriver(session) || (session.ownerId || null) !== (ownerId || null)) return false;
34
+ try { return ctx.authorizeSession(session, ownerId) === true; }
35
+ catch (e) { return false; }
36
+ }
37
+ function update(session, proposal, patch) {
38
+ var applied = transaction.persist([{ target: proposal, patch: Object.assign({}, patch, { updatedAt: Date.now() }) }], function () {
39
+ return sm.saveSessionFile(session);
40
+ });
41
+ if (!applied.ok) return false;
42
+ sm.sendToSession(session, Object.assign({
43
+ type: "driver_continuation_update",
44
+ proposalId: proposal.proposalId,
45
+ sourceSessionId: session.localId,
46
+ sourceOriginId: proposal.sourceOriginId,
47
+ projectSlug: ctx.projectSlug,
48
+ }, patch));
49
+ return true;
50
+ }
51
+
52
+ function updateTarget(target, patch) {
53
+ return transaction.persist([{ target: target.driverContinuation, patch: patch }], function () {
54
+ return sm.saveSessionFile(target);
55
+ }).ok;
56
+ }
57
+
58
+ function normalizedHandoff(args) {
59
+ var handoff = {};
60
+ var total = 0;
61
+ for (var i = 0; i < HANDOFF_FIELDS.length; i++) {
62
+ var field = HANDOFF_FIELDS[i];
63
+ handoff[field] = text(args[field], MAX_FIELD_CHARS);
64
+ total += handoff[field].length;
65
+ }
66
+ if (!handoff.goal || !handoff.nextAction) return { error: "goal and nextAction are required." };
67
+ if (total > MAX_TOTAL_CHARS) return { error: "The continuation handoff is too long." };
68
+ return { value: handoff };
69
+ }
70
+
71
+ function proposalReason(args) {
72
+ return text(args && args.reason, 800);
73
+ }
74
+
75
+ function propose(args, session, generation) {
76
+ if (!authorize(session, session && session.ownerId || null)) return toolResult({ error: "Driver continuation requires an authorized live Project Driver session." });
77
+ if (skipPermissions(session)) return toolResult({ error: "Driver continuation proposals are disabled while Skip permissions is selected or forced." });
78
+ if (Number.isInteger(generation) && Number(session._sdkQueryGeneration || 0) !== generation) {
79
+ return toolResult({ error: "This continuation tool belongs to an older query." });
80
+ }
81
+ var reason = proposalReason(args);
82
+ var milestone = text(args && args.milestone, 1200);
83
+ var benefit = text(args && args.benefit, 1200);
84
+ var normalized = normalizedHandoff(args || {});
85
+ if (normalized.error) return toolResult({ error: normalized.error });
86
+ var eligibility = continuationTrigger.evaluate(session, ctx, {
87
+ reason: reason, milestone: milestone, benefit: benefit, nextAction: normalized.value.nextAction,
88
+ });
89
+ if (eligibility.error) return toolResult({ error: eligibility.error,
90
+ proposalId: eligibility.previous && eligibility.previous.proposalId || undefined });
91
+ var key = contextKey(session);
92
+ sessionProvenance.ensureOrigin(session);
93
+ var proposal = {
94
+ type: "driver_continuation_proposal",
95
+ proposalId: "continuation_" + crypto.randomUUID(),
96
+ status: "pending",
97
+ reason: reason,
98
+ milestone: milestone,
99
+ benefit: benefit,
100
+ handoff: normalized.value,
101
+ contextKey: key,
102
+ sourceSessionId: session.localId,
103
+ sourceOriginId: session.sessionOriginId,
104
+ sourceSessionRef: workspaceSessionRef(ctx.projectSlug, session),
105
+ ownerId: session.ownerId || null,
106
+ projectSlug: ctx.projectSlug,
107
+ contextStatus: eligibility.status,
108
+ triggerEvidence: eligibility.evidence,
109
+ createdAt: Date.now(),
110
+ updatedAt: Date.now(),
111
+ };
112
+ var posted = transaction.appendPersisted(session, proposal, function () {
113
+ return sm.saveSessionFile(session);
114
+ });
115
+ if (!posted.ok) {
116
+ return toolResult({ error: "The continuation proposal could not be persisted. No review card was posted." });
117
+ }
118
+ sm.sendToSession(session, proposal);
119
+ return toolResult({
120
+ status: "posted",
121
+ proposalId: proposal.proposalId,
122
+ instruction: "The continuation card is waiting for the user's explicit choice. End this turn without accepting it yourself.",
123
+ });
124
+ }
125
+
126
+ function unsafeReason(session) {
127
+ if (session.isProcessing || session._queryStarting || session._awaitingTurnResult) return "Wait for the current Driver turn to finish, then retry.";
128
+ if (ctx.pendingMessageQueue && ctx.pendingMessageQueue.hasActive(session)) return "Pending human messages must be handled here before continuing in a new session.";
129
+ if (session.scheduledMessage || session.rateLimitAutoContinuePending) return "A scheduled callback is still attached to this session.";
130
+ if (session.autonomousRun && ACTIVE_RUN_STATES.indexOf(session.autonomousRun.state) !== -1) return "Until complete is still active in this session.";
131
+ if (hasEntries(session.pendingPermissions) || hasEntries(session.pendingAskUser) ||
132
+ hasEntries(session.pendingElicitations) || hasEntries(session.pendingUserDialogs)) {
133
+ return "This session is waiting for a permission or user-input response.";
134
+ }
135
+ if (workerProposalControl.pendingProposal(session)) return "A Split Worker proposal is still waiting for a decision.";
136
+ var pair = continuationPair.inspect(session, ctx);
137
+ if (!pair.ok) return pair.error;
138
+ if (!pair.group && (session._pairDelegation || continuationPair.activeFollowups(session))) {
139
+ return "Split Worker work or follow-ups are still pending.";
140
+ }
141
+ if (session.pendingPush && session.pendingPush.length) return "Messages are still waiting for delivery in this session.";
142
+ if (session.taskStopRequested || session.destroying || session._runtimeRefreshRequested) return "This session is changing state; retry when it is idle.";
143
+ return "";
144
+ }
145
+
146
+ function actorForResponse(ws, source) {
147
+ var userId = ws && ws._clayUser ? ws._clayUser.id : null;
148
+ if (ws._clayActiveSession !== source.localId) throw new Error("The continuation decision belongs to another source session.");
149
+ if (ctx.isMultiUser()) {
150
+ if (!userId || source.ownerId !== userId) throw new Error("Only the source Driver owner can decide this continuation.");
151
+ } else if (source.ownerId) {
152
+ throw new Error("The source Driver owner is unavailable.");
153
+ }
154
+ if (!authorize(source, userId)) throw new Error("Driver continuation access changed.");
155
+ return userId;
156
+ }
157
+
158
+ function successorFor(proposal) {
159
+ var found = null;
160
+ sm.sessions.forEach(function (candidate) {
161
+ if (found || !candidate || !candidate.driverContinuation) return;
162
+ if (candidate.driverContinuation.proposalId === proposal.proposalId &&
163
+ candidate.driverContinuation.sourceOriginId === proposal.sourceOriginId &&
164
+ candidate.driverContinuation.projectSlug === ctx.projectSlug &&
165
+ (candidate.ownerId || null) === proposal.ownerId) found = candidate;
166
+ });
167
+ return found;
168
+ }
169
+
170
+ function startupProven(candidate) {
171
+ var relation = candidate && candidate.driverContinuation;
172
+ return !!(candidate && candidate.cliSessionId && relation &&
173
+ relation.startupProof === "initialized" && Number.isInteger(relation.acceptedQueryGeneration));
174
+ }
175
+
176
+ function acceptanceError(ws, source, proposal) {
177
+ actorForResponse(ws, source);
178
+ if (skipPermissions(source)) return "Driver continuation is disabled while Skip permissions is selected or forced.";
179
+ if (proposal.contextKey !== contextKey(source)) return "The source work changed after this proposal. Ask the Driver to prepare a fresh continuation.";
180
+ return unsafeReason(source);
181
+ }
182
+
183
+ function releaseLease(source, lease) {
184
+ if (!continuationLease.end(source, lease)) return false;
185
+ if (typeof ctx.consumePendingMessage === "function") {
186
+ setImmediate(function () { ctx.consumePendingMessage(source); });
187
+ }
188
+ return true;
189
+ }
190
+
191
+ var pairTransferControl = attachContinuationPairTransfer({ ctx: ctx, update: update,
192
+ beginLease: continuationLease.begin, releaseLease: releaseLease });
193
+
194
+ function commitRelation(source, proposal, target) {
195
+ var relation = target.driverContinuation;
196
+ var transactionId = relation.transactionId || proposal.transactionId || "transition_" + crypto.randomUUID();
197
+ if (!startupProven(target)) {
198
+ return { ok: false, error: "The successor has no verified startup proof." };
199
+ }
200
+ if (relation.status === "starting") {
201
+ if (!updateTarget(target, { status: "prepared", transactionId: transactionId, preparedAt: Date.now() })) {
202
+ return { ok: false, error: "The prepared successor relationship could not be persisted." };
203
+ }
204
+ }
205
+ if (proposal.status !== "accepted" && proposal.transactionId !== transactionId) {
206
+ if (!update(source, proposal, { status: "starting", transactionId: transactionId, targetSessionId: target.localId,
207
+ targetOriginId: target.sessionOriginId, error: null })) {
208
+ return { ok: false, error: "The source continuation relationship could not be persisted." };
209
+ }
210
+ }
211
+ if (relation.status !== "accepted") {
212
+ if (!updateTarget(target, { status: "accepted", transactionId: transactionId, acceptedAt: Date.now() })) {
213
+ return { ok: false, error: "The accepted successor relationship could not be persisted." };
214
+ }
215
+ }
216
+ if (proposal.status !== "accepted") {
217
+ if (!update(source, proposal, { status: "accepted", transactionId: transactionId, targetSessionId: target.localId,
218
+ targetOriginId: target.sessionOriginId, acceptedAt: Date.now(), error: null })) {
219
+ return { ok: false, error: "The accepted source relationship could not be persisted." };
220
+ }
221
+ }
222
+ if (!transaction.accepted(proposal, relation)) return { ok: false, error: "The continuation relationship is not durably complete." };
223
+ return { ok: true };
224
+ }
225
+
226
+ async function accept(ws, source, proposal) {
227
+ var ownerId = actorForResponse(ws, source);
228
+ var reason = acceptanceError(ws, source, proposal);
229
+ if (reason) {
230
+ if (proposal.contextKey !== contextKey(source)) {
231
+ update(source, proposal, { status: "superseded", error: reason });
232
+ }
233
+ throw new Error(reason);
234
+ }
235
+ var existing = successorFor(proposal);
236
+ if (existing) {
237
+ if (!authorize(existing, ownerId)) throw new Error("Successor continuation access changed.");
238
+ if (!startupProven(existing)) {
239
+ if (!existing.queryInstance && !existing.isProcessing && !existing._queryStarting) {
240
+ sm.deleteSessionQuiet(existing.localId);
241
+ update(source, proposal, { status: "superseded", targetSessionId: null, targetOriginId: null,
242
+ error: "The prior successor did not survive startup. Prepare a fresh continuation proposal." });
243
+ throw new Error("The prior successor did not survive startup. Prepare a fresh continuation proposal.");
244
+ }
245
+ throw new Error("The successor is still starting. Wait for initialization confirmation before retrying.");
246
+ }
247
+ var retainedLease = source._driverContinuationLease;
248
+ if (!retainedLease) {
249
+ var recoveredPair = pairTransferControl.recoverIfNeeded(source, existing, proposal);
250
+ if (!recoveredPair.ok) throw new Error(recoveredPair.error);
251
+ retainedLease = recoveredPair.lease;
252
+ }
253
+ if (retainedLease && retainedLease.proposalId === proposal.proposalId && retainedLease.target === existing) {
254
+ var retryReason = pairTransferControl.retryError(source, existing, retainedLease, function () {
255
+ return acceptanceError(ws, source, proposal);
256
+ });
257
+ if (retryReason) throw new Error(retryReason);
258
+ var existingPairCommit = pairTransferControl.commitOrCancel(retainedLease);
259
+ if (!existingPairCommit.ok) throw new Error(existingPairCommit.error);
260
+ }
261
+ var existingCommit = commitRelation(source, proposal, existing);
262
+ if (!existingCommit.ok) throw new Error(existingCommit.error);
263
+ if (retainedLease && retainedLease.proposalId === proposal.proposalId && retainedLease.target === existing) {
264
+ releaseLease(source, retainedLease);
265
+ }
266
+ sm.switchSession(existing.localId, ws);
267
+ return { ok: true, status: "accepted", targetSessionId: existing.localId };
268
+ }
269
+ if (proposal.status === "starting") {
270
+ if (!update(source, proposal, { status: "pending", error: "The prior startup did not leave a durable successor. Retry when ready." })) {
271
+ throw new Error("The stale startup state could not be persisted.");
272
+ }
273
+ }
274
+ if (proposal.status !== "pending") throw new Error("This continuation proposal has already been resolved.");
275
+ var target = sm.createSessionRaw({
276
+ ownerId: source.ownerId || null,
277
+ sessionVisibility: source.sessionVisibility,
278
+ vendor: source.vendor,
279
+ model: source.model,
280
+ effort: source.effort,
281
+ permissionMode: source.permissionMode || null,
282
+ });
283
+ target.mcpPermissionModeOverrides = Object.assign({}, source.mcpPermissionModeOverrides || {});
284
+ target.effectivePermissionMode = null;
285
+ target.title = (source.title || "Continued work") + " · Continued";
286
+ target.driverContinuation = {
287
+ proposalId: proposal.proposalId,
288
+ sourceOriginId: proposal.sourceOriginId,
289
+ sourceSessionRef: proposal.sourceSessionRef,
290
+ projectSlug: ctx.projectSlug,
291
+ status: "starting",
292
+ createdAt: Date.now(),
293
+ };
294
+ sm.sendAndRecord(target, {
295
+ type: "driver_continuation_context",
296
+ proposalId: proposal.proposalId,
297
+ sourceSessionId: source.localId,
298
+ sourceOriginId: proposal.sourceOriginId,
299
+ sourceSessionRef: proposal.sourceSessionRef,
300
+ projectSlug: ctx.projectSlug,
301
+ reason: proposal.reason,
302
+ goal: proposal.handoff.goal,
303
+ nextAction: proposal.handoff.nextAction,
304
+ repositoryState: proposal.handoff.repositoryState,
305
+ _ts: Date.now(),
306
+ });
307
+ if (!update(source, proposal, { status: "starting", targetSessionId: target.localId, targetOriginId: target.sessionOriginId, error: null })) {
308
+ sm.deleteSessionQuiet(target.localId);
309
+ var persistenceError = "The source continuation proposal could not be persisted.";
310
+ update(source, proposal, { status: "pending", targetSessionId: null, targetOriginId: null, error: persistenceError });
311
+ throw new Error(persistenceError);
312
+ }
313
+ target.isProcessing = true;
314
+ target.sentToolResults = {};
315
+ ctx.onProcessingChanged();
316
+ var lease = continuationLease.begin(source, target, proposal.proposalId);
317
+ var pairTransfer = pairTransferControl.begin(source, target, proposal, lease);
318
+ if (!pairTransfer.ok) {
319
+ releaseLease(source, lease);
320
+ sm.deleteSessionQuiet(target.localId);
321
+ update(source, proposal, { status: "pending", targetSessionId: null, targetOriginId: null, error: pairTransfer.error });
322
+ throw new Error(pairTransfer.error);
323
+ }
324
+ function startupAcceptanceError() {
325
+ return pairTransferControl.validationError(source, target, lease, function () {
326
+ return acceptanceError(ws, source, proposal);
327
+ });
328
+ }
329
+ var started = false;
330
+ var retainTarget = false;
331
+ var timedOut = false;
332
+ function lateStartup(lifecycle) {
333
+ continuationStartup.handleLate({ sm: sm, source: source, target: target, proposal: proposal, lease: lease,
334
+ validate: startupAcceptanceError,
335
+ commitPair: function () { return pairTransferControl.commit(lease); },
336
+ release: function () { releaseLease(source, lease); },
337
+ update: function (patch) { return update(source, proposal, patch); } }, lifecycle);
338
+ }
339
+ try {
340
+ var sdk = ctx.getSdk();
341
+ if (!sdk || typeof sdk.startQueryWithAcceptance !== "function") throw new Error("SDK bridge cannot confirm successor startup.");
342
+ var lifecycle = await sdk.startQueryWithAcceptance(target, continuationStartup.compactPrompt(proposal), undefined,
343
+ ctx.getLinuxUserForSession(target), function () {
344
+ return continuationLease.guard(lease, startupAcceptanceError);
345
+ }, lateStartup);
346
+ started = !!(lifecycle && lifecycle.accepted === true);
347
+ retainTarget = !!(lifecycle && lifecycle.initialAccepted === true && lifecycle.queryAlive === true);
348
+ timedOut = !!(lifecycle && lifecycle.timedOut === true);
349
+ if (!started) throw new Error(lease.error || lifecycle && lifecycle.reason || "The successor agent did not accept the compact handoff.");
350
+ if (!Number.isInteger(lifecycle.queryGeneration)) throw new Error("The successor has no verified startup proof.");
351
+ target.driverContinuation.acceptedQueryGeneration = lifecycle.queryGeneration;
352
+ target.driverContinuation.acceptedInitialAt = Date.now();
353
+ target.driverContinuation.startupProof = "initialized";
354
+ if (!await continuationStartup.waitForDurableIdentity(sm, target, ctx.identityWaitMs)) throw new Error("The successor accepted its handoff but did not receive a durable session identity in time.");
355
+ if (!transaction.saveObserved(function () { return sm.saveSessionFile(target); })) {
356
+ lease.waitingCommit = true;
357
+ throw new Error("The verified successor startup could not be persisted. Retry without starting another successor.");
358
+ }
359
+ try { reason = startupAcceptanceError(); }
360
+ catch (revalidationError) { reason = revalidationError.message || String(revalidationError); }
361
+ if (reason) {
362
+ lease.cancel(reason);
363
+ throw new Error(reason);
364
+ }
365
+ lease.waitingCommit = true;
366
+ var pairCommitted = pairTransferControl.commit(lease);
367
+ if (!pairCommitted.ok) {
368
+ lease.cancel(pairCommitted.error);
369
+ throw new Error(pairCommitted.error);
370
+ }
371
+ var committed = commitRelation(source, proposal, target);
372
+ if (!committed.ok) throw new Error(committed.error);
373
+ releaseLease(source, lease);
374
+ sm.broadcastSessionList();
375
+ sm.switchSession(target.localId, ws);
376
+ return { ok: true, status: "accepted", targetSessionId: target.localId, ownerId: ownerId };
377
+ } catch (error) {
378
+ if (timedOut && retainTarget && !lease.cancelled) lease.waitingLateStartup = true;
379
+ else if (!lease.waitingCommit && !lease.cancelled) {
380
+ pairTransferControl.rollback(lease);
381
+ releaseLease(source, lease);
382
+ }
383
+ if (lease.cancelled && !lease.cleanupComplete) {
384
+ error = new Error(lease.cleanupError || error.message || String(error));
385
+ } else if (!lease.cleanupComplete && started !== true && retainTarget !== true) {
386
+ pairTransferControl.rollback(lease);
387
+ sm.deleteSessionQuiet(target.localId);
388
+ }
389
+ if (proposal.status !== "accepted") update(source, proposal, {
390
+ status: lease.cancelled ? "superseded" : "pending", targetSessionId: sm.sessions.has(target.localId) ? target.localId : null,
391
+ targetOriginId: sm.sessions.has(target.localId) ? target.sessionOriginId : null,
392
+ error: error.message || String(error),
393
+ });
394
+ throw error;
395
+ }
396
+ }
397
+ async function respond(ws, msg) {
398
+ var ownerId = ws && ws._clayUser ? ws._clayUser.id : null;
399
+ var source = continuationAccess.findOwnedSessionByOrigin(sm.sessions, msg.sourceOriginId, ownerId);
400
+ if (!source) throw new Error("The source Driver session no longer exists.");
401
+ actorForResponse(ws, source);
402
+ var proposal = findProposal(source, msg.proposalId);
403
+ if (!proposal || proposal.ownerId !== (source.ownerId || null) || proposal.projectSlug !== ctx.projectSlug ||
404
+ proposal.sourceOriginId !== source.sessionOriginId || proposal.sourceOriginId !== msg.sourceOriginId) {
405
+ throw new Error("The exact continuation proposal was not found.");
406
+ }
407
+ if (msg.accepted !== true) {
408
+ if (proposal.status !== "pending") throw new Error("This continuation proposal has already been resolved.");
409
+ var declinedAt = Date.now();
410
+ var declined = transaction.persist([
411
+ { target: source, patch: { driverContinuationDecline: { proposalId: proposal.proposalId, declinedAt: declinedAt, permanent: true } } },
412
+ { target: proposal, patch: { status: "declined", declinedAt: declinedAt, error: null, updatedAt: declinedAt } },
413
+ ], function () { return sm.saveSessionFile(source); });
414
+ if (!declined.ok) throw new Error("The decision to stay could not be persisted.");
415
+ sm.sendToSession(source, { type: "driver_continuation_update", proposalId: proposal.proposalId,
416
+ sourceSessionId: source.localId, sourceOriginId: proposal.sourceOriginId, projectSlug: ctx.projectSlug,
417
+ status: "declined", declinedAt: declinedAt, error: null });
418
+ return { ok: true, status: "declined", sourceSessionId: source.localId };
419
+ }
420
+ var result = await accept(ws, source, proposal);
421
+ result.sourceSessionId = source.localId;
422
+ return result;
423
+ }
424
+
425
+ function openSource(ws, msg) {
426
+ var target = sm.sessions.get(ws._clayActiveSession);
427
+ var relation = target && target.driverContinuation;
428
+ var ownerId = ws && ws._clayUser ? ws._clayUser.id : null;
429
+ if (!target || !relation || relation.proposalId !== msg.proposalId || relation.sourceOriginId !== msg.sourceOriginId) {
430
+ throw new Error("The exact continuation source link is stale.");
431
+ }
432
+ if (!authorize(target, ownerId)) throw new Error("Continuation source access changed.");
433
+ var source = null;
434
+ sm.sessions.forEach(function (candidate) {
435
+ if (source || !candidate) return;
436
+ if (candidate.sessionOriginId === relation.sourceOriginId && (candidate.ownerId || null) === (target.ownerId || null)) source = candidate;
437
+ });
438
+ if (!source || !authorize(source, ownerId)) throw new Error("The original Driver session is no longer available.");
439
+ sm.switchSession(source.localId, ws);
440
+ return { ok: true, status: "source_opened", targetSessionId: source.localId };
441
+ }
442
+
443
+ function handleMessage(ws, msg) {
444
+ if (msg.type !== "driver_continuation_response" && msg.type !== "driver_continuation_open_source") return false;
445
+ var requestId = text(msg.requestId, 128);
446
+ var exactProject = text(msg.projectSlug, 200);
447
+ var sourceOriginId = text(msg.sourceOriginId, 200);
448
+ var operationName = msg.type === "driver_continuation_open_source" ? "open_source" : "decision";
449
+ if (!requestId || exactProject !== ctx.projectSlug || !sourceOriginId) {
450
+ ctx.sendTo(ws, { type: "driver_continuation_result", requestId: requestId, operation: operationName,
451
+ projectSlug: ctx.projectSlug, sourceOriginId: sourceOriginId, proposalId: msg.proposalId,
452
+ sourceSessionId: null, ok: false, error: "The continuation request correlation is incomplete or stale." });
453
+ return true;
454
+ }
455
+ var operation = msg.type === "driver_continuation_open_source"
456
+ ? Promise.resolve().then(function () { return openSource(ws, msg); })
457
+ : respond(ws, msg);
458
+ operation.then(function (result) {
459
+ ctx.sendTo(ws, Object.assign({ type: "driver_continuation_result", requestId: requestId, operation: operationName,
460
+ projectSlug: ctx.projectSlug, sourceOriginId: sourceOriginId, proposalId: msg.proposalId,
461
+ sourceSessionId: null }, result));
462
+ }).catch(function (error) {
463
+ ctx.sendTo(ws, { type: "driver_continuation_result", requestId: requestId, operation: operationName,
464
+ projectSlug: ctx.projectSlug, sourceOriginId: sourceOriginId, proposalId: msg.proposalId,
465
+ sourceSessionId: null, ok: false, error: error.message || String(error) });
466
+ });
467
+ return true;
468
+ }
469
+
470
+ function getToolDefs(session, generation) {
471
+ if (!authorize(session, session && session.ownerId || null) || skipPermissions(session)) return [];
472
+ if (continuationTrigger.historicalProposal(session) || session.driverContinuationDecline) return [];
473
+ return [{
474
+ name: "propose_driver_continuation",
475
+ description: "Propose, but never accept, the source session's one user-reviewed proactive continuation. Clay requires measured current-context pressure or a completed recorded compaction, a completed milestone, a specific reason and benefit, and a prepared next action. Never claim guaranteed savings.",
476
+ inputSchema: buildShape({
477
+ reason: { type: "string", description: "Why moving at this specific boundary is useful now." },
478
+ milestone: { type: "string", description: "The concrete milestone completed before this safe transition boundary." },
479
+ benefit: { type: "string", description: "The concrete continuity benefit, without unsupported savings claims." },
480
+ goal: { type: "string" }, constraints: { type: "string" },
481
+ decisions: { type: "string", description: "Decisions and why they were made." },
482
+ rejectedApproaches: { type: "string" }, unresolved: { type: "string" },
483
+ nextAction: { type: "string" }, verification: { type: "string" }, repositoryState: { type: "string" },
484
+ }, ["reason", "milestone", "benefit", "goal", "nextAction"]),
485
+ handler: function (args) { return propose(args || {}, session, generation); },
486
+ }];
487
+ }
488
+
489
+ function getSystemPrompt(session) {
490
+ if (!authorize(session, session && session.ownerId || null) || skipPermissions(session)) return "";
491
+ if (continuationTrigger.historicalProposal(session) || session.driverContinuationDecline) return "";
492
+ return continuationTrigger.prompt(session) + " Never propose while a Split Worker is attached or work or user input is pending. " +
493
+ "After posting, end the turn. Same-project bounded history can be retrieved later; do not request global source reading.";
494
+ }
495
+
496
+ return { getSystemPrompt: getSystemPrompt, getToolDefs: getToolDefs, handleMessage: handleMessage, respond: respond, openSource: openSource };
497
+ }
498
+
499
+ module.exports = { attachDriverContinuation: attachDriverContinuation, contextKey: contextKey, findProposal: findProposal };