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.
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,54 @@
1
+ var driverEligibility = require("./session-driver-eligibility");
2
+
3
+ function activeActor(users, ownerId) {
4
+ if (!ownerId || !users || typeof users.findUserById !== "function") return null;
5
+ var actor = users.findUserById(ownerId);
6
+ if (!actor || actor.disabled === true || actor.active === false || actor.status === "disabled" || actor.status === "inactive") return null;
7
+ return actor;
8
+ }
9
+
10
+ function authorize(settings, session, ownerId) {
11
+ var users = settings.usersModule;
12
+ if (!users || typeof users.isMultiUser !== "function") return false;
13
+ if (!users.isMultiUser()) return !!session && !session.ownerId;
14
+ var actor;
15
+ try { actor = activeActor(users, ownerId); }
16
+ catch (error) { return false; }
17
+ if (!actor || !session || typeof settings.getProjectAccess !== "function" ||
18
+ typeof settings.canAccessProjectSlug !== "function" || typeof users.canAccessSession !== "function") return false;
19
+ try {
20
+ var access = settings.getProjectAccess();
21
+ if (!access || access.error || settings.canAccessProjectSlug(actor.id, settings.projectSlug) !== true) return false;
22
+ return users.canAccessSession(actor.id, session, access) === true;
23
+ } catch (error) {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ function findOwnedSessionByOrigin(sessions, originId, ownerId) {
29
+ if (!sessions || !originId) return null;
30
+ var found = null;
31
+ var ambiguous = false;
32
+ sessions.forEach(function (session) {
33
+ if (!session || session.sessionOriginId !== originId ||
34
+ (session.ownerId || null) !== (ownerId || null)) return;
35
+ if (found && found !== session) ambiguous = true;
36
+ else found = session;
37
+ });
38
+ return ambiguous ? null : found;
39
+ }
40
+
41
+ function isLiveDriver(session, isMate, sessions, isDriverOperatedSession) {
42
+ if (!session || isMate || session.isMate === true || !sessions || sessions.get(session.localId) !== session) return false;
43
+ try {
44
+ if (typeof isDriverOperatedSession === "function" && isDriverOperatedSession(session) === true) return false;
45
+ } catch (error) {
46
+ return false;
47
+ }
48
+ return driverEligibility.isEligibleDriverSession(session) && session.hidden !== true &&
49
+ session.delegated !== true && !session._pairDelegation && !session._delegatedBy &&
50
+ !session.scheduledTaskRun && !session.projectLogReview;
51
+ }
52
+
53
+ module.exports = { authorize: authorize, activeActor: activeActor,
54
+ findOwnedSessionByOrigin: findOwnedSessionByOrigin, isLiveDriver: isLiveDriver };
@@ -0,0 +1,52 @@
1
+ function closeTarget(target) {
2
+ if (!target || !target.queryInstance) return;
3
+ try { target.queryInstance.close(); }
4
+ catch (error) {}
5
+ }
6
+
7
+ function begin(source, target, proposalId) {
8
+ var lease = {
9
+ proposalId: proposalId,
10
+ target: target,
11
+ cancelled: false,
12
+ error: null,
13
+ waitingLateStartup: false,
14
+ };
15
+ lease.cancel = function (reason) {
16
+ if (!lease.cancelled) {
17
+ lease.cancelled = true;
18
+ lease.error = reason || "The source received new work during continuation startup.";
19
+ closeTarget(target);
20
+ }
21
+ if (typeof lease.onCancel === "function") return lease.onCancel(lease.error);
22
+ };
23
+ source._driverContinuationLease = lease;
24
+ return lease;
25
+ }
26
+
27
+ function guard(lease, validate) {
28
+ if (!lease || lease.cancelled) return false;
29
+ try {
30
+ var error = validate();
31
+ if (!error) return true;
32
+ lease.cancel(error);
33
+ } catch (error) {
34
+ lease.cancel(error.message || String(error));
35
+ }
36
+ return false;
37
+ }
38
+
39
+ function cancelForHumanMessage(session) {
40
+ var lease = session && session._driverContinuationLease;
41
+ if (!lease || typeof lease.cancel !== "function") return false;
42
+ lease.cancel("New source work arrived during continuation startup.");
43
+ return true;
44
+ }
45
+
46
+ function end(source, lease) {
47
+ if (!source || source._driverContinuationLease !== lease) return false;
48
+ delete source._driverContinuationLease;
49
+ return true;
50
+ }
51
+
52
+ module.exports = { begin: begin, guard: guard, cancelForHumanMessage: cancelForHumanMessage, end: end };
@@ -0,0 +1,86 @@
1
+ var continuationPair = require("./driver-continuation-pair");
2
+
3
+ function attachContinuationPairTransfer(settings) {
4
+ var ctx = settings.ctx;
5
+
6
+ function commit(lease) {
7
+ var transfer = lease && lease.pairTransfer;
8
+ if (!transfer || transfer.committed) return { ok: true };
9
+ var result = ctx.splitStore.commitOwnedDriverTransfer(transfer);
10
+ if (!result.ok) return { ok: false, error: "The Split Worker ownership transfer could not be persisted: " + result.error };
11
+ return { ok: true };
12
+ }
13
+
14
+ function rollback(lease) {
15
+ var transfer = lease && lease.pairTransfer;
16
+ if (!transfer || !transfer.staged) return { ok: true };
17
+ return ctx.splitStore.rollbackOwnedDriverTransfer(transfer);
18
+ }
19
+
20
+ function begin(source, target, proposal, lease) {
21
+ var staged = continuationPair.stage(source, target, ctx);
22
+ if (!staged.ok) return staged;
23
+ lease.pairTransfer = staged.transaction || null;
24
+ lease.onCancel = function (cancelError) {
25
+ var rolledBack = rollback(lease);
26
+ if (!rolledBack.ok) {
27
+ lease.cleanupError = "The Split Worker ownership transfer could not be rolled back: " + rolledBack.error;
28
+ return false;
29
+ }
30
+ var persisted = settings.update(source, proposal, { status: "superseded", targetSessionId: null,
31
+ targetOriginId: null, error: cancelError });
32
+ if (!persisted) {
33
+ lease.cleanupError = "The cancelled continuation state could not be persisted; the successor was retained for recovery.";
34
+ return false;
35
+ }
36
+ lease.cleanupComplete = true;
37
+ ctx.sm.deleteSessionQuiet(target.localId);
38
+ settings.releaseLease(source, lease);
39
+ return true;
40
+ };
41
+ return staged;
42
+ }
43
+
44
+ function validationError(source, target, lease, validateSource) {
45
+ var sourceError = validateSource();
46
+ if (sourceError) return sourceError;
47
+ if (!lease.pairTransfer) return "";
48
+ var transferred = continuationPair.inspect(target, ctx);
49
+ if (!transferred.ok) return transferred.error;
50
+ if (!transferred.group || transferred.group !== lease.pairTransfer.runtimeGroup ||
51
+ transferred.worker !== lease.pairTransfer.worker) {
52
+ return "The staged Split Worker ownership changed during successor startup.";
53
+ }
54
+ return "";
55
+ }
56
+
57
+ function retryError(source, target, lease, validateSource) {
58
+ if (!lease || !lease.pairTransfer || lease.pairTransfer.committed) return "";
59
+ return validationError(source, target, lease, validateSource);
60
+ }
61
+
62
+ function commitOrCancel(lease) {
63
+ var result = commit(lease);
64
+ if (result.ok) return result;
65
+ lease.cancel(result.error);
66
+ return { ok: false, error: lease.cleanupError || result.error };
67
+ }
68
+
69
+ function recoverIfNeeded(source, target, proposal) {
70
+ var current = continuationPair.inspect(source, ctx);
71
+ if (!current.group) return { ok: true, lease: null };
72
+ var lease = settings.beginLease(source, target, proposal.proposalId);
73
+ var recovered = begin(source, target, proposal, lease);
74
+ if (!recovered.ok) {
75
+ settings.releaseLease(source, lease);
76
+ return recovered;
77
+ }
78
+ lease.waitingCommit = true;
79
+ return { ok: true, lease: lease };
80
+ }
81
+
82
+ return { begin: begin, commit: commit, commitOrCancel: commitOrCancel, recoverIfNeeded: recoverIfNeeded,
83
+ retryError: retryError, rollback: rollback, validationError: validationError };
84
+ }
85
+
86
+ module.exports = { attachContinuationPairTransfer: attachContinuationPairTransfer };
@@ -0,0 +1,82 @@
1
+ var sessionProvenance = require("./session-provenance");
2
+
3
+ var ACTIVE_RUN_STATES = ["armed", "running", "reviewing", "waiting-worker", "waiting-user", "paused"];
4
+
5
+ function hasEntries(value) {
6
+ return !!(value && Object.keys(value).length > 0);
7
+ }
8
+
9
+ function activeFollowups(session) {
10
+ var items = session && session._pairFollowups;
11
+ if (!Array.isArray(items)) return false;
12
+ return items.some(function (item) {
13
+ return item && ["queued", "waiting_for_interrupt", "starting", "running"].indexOf(item.status) !== -1;
14
+ });
15
+ }
16
+
17
+ function sessionWaitReason(session, label) {
18
+ if (!session) return label + " no longer exists.";
19
+ if (session.isProcessing || session._queryStarting || session._awaitingTurnResult) return label + " is still processing.";
20
+ if (session._pairDelegation) return "The Split Worker still has an open delegation.";
21
+ if (activeFollowups(session)) return "The Split Worker still has queued follow-ups.";
22
+ if (session.pendingPush && session.pendingPush.length) return "The Split Worker still has messages waiting for delivery.";
23
+ if (hasEntries(session.pendingPermissions) || hasEntries(session.pendingAskUser) ||
24
+ hasEntries(session.pendingElicitations) || hasEntries(session.pendingUserDialogs)) {
25
+ return label + " is waiting for a permission or user-input response.";
26
+ }
27
+ if (session.scheduledMessage || session.rateLimitAutoContinuePending) return label + " has a scheduled callback.";
28
+ if (session.autonomousRun && ACTIVE_RUN_STATES.indexOf(session.autonomousRun.state) !== -1) return label + " has active Until complete work.";
29
+ if (session.taskStopRequested || session.destroying || session._runtimeRefreshRequested) return label + " is changing state.";
30
+ return "";
31
+ }
32
+
33
+ function inspect(source, ctx) {
34
+ var store = ctx && ctx.splitStore;
35
+ var group = store && source ? store.groupForMember(source.localId) : null;
36
+ if (!group) return { ok: true, group: null, worker: null };
37
+ if (!group.pair || group.pair.driverId !== source.localId) {
38
+ return { ok: false, error: "Only the configured Driver can continue an attached split pair." };
39
+ }
40
+ var worker = ctx.sm && ctx.sm.sessions.get(group.pair.workerId);
41
+ if (!worker || group.members.indexOf(worker.localId) === -1) return { ok: false, error: "The attached Split Worker is unavailable." };
42
+ if ((source.ownerId || null) !== (worker.ownerId || null)) return { ok: false, error: "The attached Split Worker owner changed." };
43
+ if (!sessionProvenance.isWorker(worker)) return { ok: false, error: "The attached split partner is not a verified Worker." };
44
+ var reason = sessionWaitReason(worker, "The Split Worker");
45
+ if (reason) return { ok: false, error: reason };
46
+ return { ok: true, group: group, worker: worker };
47
+ }
48
+
49
+ function inheritState(source, target) {
50
+ if (source._pairTurnControl) {
51
+ target._pairTurnControl = Object.assign({}, source._pairTurnControl, {
52
+ operations: Object.assign(Object.create(null), source._pairTurnControl.operations || {}),
53
+ });
54
+ }
55
+ if (Array.isArray(source._workerGenerations)) {
56
+ target._workerGenerations = source._workerGenerations.map(function (record) {
57
+ return Object.assign({}, record, {
58
+ observed: record.observed ? Object.assign({}, record.observed) : record.observed,
59
+ evaluation: record.evaluation ? Object.assign({}, record.evaluation) : record.evaluation,
60
+ });
61
+ });
62
+ }
63
+ }
64
+
65
+ function stage(source, target, ctx) {
66
+ var inspected = inspect(source, ctx);
67
+ if (!inspected.ok || !inspected.group) return inspected;
68
+ if (!ctx.splitStore || typeof ctx.splitStore.beginOwnedDriverTransfer !== "function") {
69
+ return { ok: false, error: "Split pair transfer is unavailable." };
70
+ }
71
+ inheritState(source, target);
72
+ var staged = ctx.splitStore.beginOwnedDriverTransfer(source.ownerId || null, {
73
+ id: inspected.group.id,
74
+ sourceDriverId: source.localId,
75
+ targetDriverId: target.localId,
76
+ workerId: inspected.worker.localId,
77
+ });
78
+ if (!staged.ok) return staged;
79
+ return { ok: true, group: staged.group, worker: inspected.worker, transaction: staged.transaction };
80
+ }
81
+
82
+ module.exports = { activeFollowups: activeFollowups, inspect: inspect, inheritState: inheritState, stage: stage };
@@ -0,0 +1,40 @@
1
+ var crypto = require("crypto");
2
+
3
+ function toolResult(value) {
4
+ return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] });
5
+ }
6
+
7
+ function text(value, max) {
8
+ if (typeof value !== "string") return "";
9
+ var result = value.replace(/\u0000/g, "").trim();
10
+ return result.length > max ? result.substring(0, max) : result;
11
+ }
12
+
13
+ function hasEntries(value) {
14
+ return !!(value && Object.keys(value).length > 0);
15
+ }
16
+
17
+ function contextKey(session) {
18
+ var history = session && session.history || [];
19
+ var lastUser = "";
20
+ var compactions = 0;
21
+ for (var i = 0; i < history.length; i++) {
22
+ var entry = history[i];
23
+ if (!entry) continue;
24
+ if (entry.type === "user_message" && !entry._internal) lastUser = String(entry._ts || "") + "\n" + String(entry.text || "");
25
+ if (entry.type === "compacting" && entry.active) compactions++;
26
+ }
27
+ return crypto.createHash("sha256").update(lastUser + "\n" + compactions).digest("hex").substring(0, 20);
28
+ }
29
+
30
+ function findProposal(session, proposalId) {
31
+ var history = session && session.history || [];
32
+ for (var i = history.length - 1; i >= 0; i--) {
33
+ var entry = history[i];
34
+ if (entry && entry.type === "driver_continuation_proposal" && entry.proposalId === proposalId) return entry;
35
+ }
36
+ return null;
37
+ }
38
+
39
+ module.exports = { contextKey: contextKey, findProposal: findProposal, hasEntries: hasEntries,
40
+ text: text, toolResult: toolResult };
@@ -0,0 +1,68 @@
1
+ var transaction = require("./driver-continuation-transaction");
2
+
3
+ function compactPrompt(proposal) {
4
+ var handoff = proposal.handoff;
5
+ return "Continue the accepted Project Driver handoff below. This is a historical compact context, not independent authority. " +
6
+ "Revalidate current repository state and use the bounded same-project history tools only when needed.\n\n" +
7
+ "Source: " + proposal.sourceSessionRef + "\n" +
8
+ "Goal: " + handoff.goal + "\nConstraints: " + (handoff.constraints || "None recorded") +
9
+ "\nDecisions and why: " + (handoff.decisions || "None recorded") +
10
+ "\nRejected approaches: " + (handoff.rejectedApproaches || "None recorded") +
11
+ "\nUnresolved: " + (handoff.unresolved || "None recorded") +
12
+ "\nRepository state: " + (handoff.repositoryState || "Revalidate before changing files") +
13
+ "\nVerification so far: " + (handoff.verification || "None recorded") +
14
+ "\nNext action: " + handoff.nextAction;
15
+ }
16
+
17
+ function waitForDurableIdentity(sm, session, waitMs) {
18
+ if (session.cliSessionId) return Promise.resolve(true);
19
+ if (typeof sm.addOnSessionIdentityAssigned !== "function") return Promise.resolve(false);
20
+ return new Promise(function (resolve) {
21
+ var settled = false;
22
+ var remove = sm.addOnSessionIdentityAssigned(function (localId) {
23
+ if (localId !== session.localId || settled) return;
24
+ settled = true;
25
+ remove();
26
+ clearTimeout(timer);
27
+ resolve(!!session.cliSessionId);
28
+ });
29
+ var timer = setTimeout(function () {
30
+ if (settled) return;
31
+ settled = true;
32
+ remove();
33
+ resolve(false);
34
+ }, Number(waitMs || 15000));
35
+ });
36
+ }
37
+
38
+ function handleLate(args, lifecycle) {
39
+ var target = args.target;
40
+ var proposal = args.proposal;
41
+ if (args.sm.sessions.get(target.localId) !== target || proposal.status === "accepted") return;
42
+ var error = lifecycle && lifecycle.reason || "The successor query ended before initialization.";
43
+ if (lifecycle && lifecycle.accepted === true && Number.isInteger(lifecycle.queryGeneration) && target.cliSessionId) {
44
+ target.driverContinuation.acceptedQueryGeneration = lifecycle.queryGeneration;
45
+ target.driverContinuation.acceptedInitialAt = Date.now();
46
+ target.driverContinuation.startupProof = "initialized";
47
+ try { error = args.validate(); }
48
+ catch (validationError) { error = validationError.message || String(validationError); }
49
+ if (!error && transaction.saveObserved(function () { return args.sm.saveSessionFile(target); })) {
50
+ var pairCommit = args.commitPair ? args.commitPair() : { ok: true };
51
+ if (!pairCommit.ok) error = pairCommit.error;
52
+ }
53
+ if (!error) {
54
+ args.lease.waitingLateStartup = false;
55
+ args.lease.waitingCommit = true;
56
+ return;
57
+ }
58
+ if (!error) error = "The verified successor startup could not be persisted.";
59
+ if (error === "The verified successor startup could not be persisted.") {
60
+ args.lease.waitingLateStartup = false;
61
+ args.lease.waitingCommit = true;
62
+ return;
63
+ }
64
+ }
65
+ args.lease.cancel(error);
66
+ }
67
+
68
+ module.exports = { compactPrompt: compactPrompt, waitForDurableIdentity: waitForDurableIdentity, handleLate: handleLate };
@@ -0,0 +1,61 @@
1
+ function snapshots(changes) {
2
+ return changes.map(function (change) {
3
+ var values = {};
4
+ var present = {};
5
+ Object.keys(change.patch).forEach(function (key) {
6
+ present[key] = Object.prototype.hasOwnProperty.call(change.target, key);
7
+ values[key] = change.target[key];
8
+ });
9
+ return { target: change.target, values: values, present: present };
10
+ });
11
+ }
12
+
13
+ function restore(items) {
14
+ items.forEach(function (item) {
15
+ Object.keys(item.values).forEach(function (key) {
16
+ if (item.present[key]) item.target[key] = item.values[key];
17
+ else delete item.target[key];
18
+ });
19
+ });
20
+ }
21
+
22
+ function persist(changes, save) {
23
+ var before = snapshots(changes);
24
+ changes.forEach(function (change) { Object.assign(change.target, change.patch); });
25
+ try {
26
+ if (save() !== true) {
27
+ restore(before);
28
+ return { ok: false, error: "Persistence returned false." };
29
+ }
30
+ } catch (error) {
31
+ restore(before);
32
+ return { ok: false, error: error.message || String(error) };
33
+ }
34
+ return { ok: true };
35
+ }
36
+
37
+ function accepted(sourceProposal, targetRelation) {
38
+ return !!(sourceProposal && targetRelation && sourceProposal.status === "accepted" &&
39
+ targetRelation.status === "accepted" && sourceProposal.transactionId &&
40
+ sourceProposal.transactionId === targetRelation.transactionId);
41
+ }
42
+
43
+ function appendPersisted(session, event, save) {
44
+ session.history.push(event);
45
+ try {
46
+ if (save() === true) return { ok: true };
47
+ } catch (error) {
48
+ session.history.pop();
49
+ return { ok: false, error: error.message || String(error) };
50
+ }
51
+ session.history.pop();
52
+ return { ok: false, error: "Persistence returned false." };
53
+ }
54
+
55
+ function saveObserved(save) {
56
+ try { return save() === true; }
57
+ catch (error) { return false; }
58
+ }
59
+
60
+ module.exports = { persist: persist, accepted: accepted, appendPersisted: appendPersisted,
61
+ saveObserved: saveObserved };
@@ -0,0 +1,110 @@
1
+ var pairUsage = require("./project-pair-usage");
2
+ var workerProposalControl = require("./worker-proposal-control");
3
+ var continuationPair = require("./driver-continuation-pair");
4
+
5
+ var PRESSURE_RATIO = 0.8;
6
+ var MIN_JUSTIFICATION_CHARS = 12;
7
+ var ACTIVE_RUN_STATES = ["armed", "running", "reviewing", "waiting-worker", "waiting-user", "paused"];
8
+
9
+ function hasEntries(value) {
10
+ return !!(value && Object.keys(value).length > 0);
11
+ }
12
+
13
+ function historicalProposal(session) {
14
+ var history = session && session.history || [];
15
+ for (var i = 0; i < history.length; i++) {
16
+ if (history[i] && history[i].type === "driver_continuation_proposal") return history[i];
17
+ }
18
+ return null;
19
+ }
20
+
21
+ function unsafeReason(session, ctx) {
22
+ if (session._queryStarting) return "Wait for the current Driver connection to finish starting.";
23
+ if (ctx.pendingMessageQueue && ctx.pendingMessageQueue.hasActive(session)) {
24
+ return "Pending human messages must be handled before proposing continuation.";
25
+ }
26
+ if (session.scheduledMessage || session.rateLimitAutoContinuePending) return "A scheduled callback is still attached to this session.";
27
+ if (session.autonomousRun && ACTIVE_RUN_STATES.indexOf(session.autonomousRun.state) !== -1) {
28
+ return "Until complete is still active in this session.";
29
+ }
30
+ if (hasEntries(session.pendingPermissions) || hasEntries(session.pendingAskUser) ||
31
+ hasEntries(session.pendingElicitations) || hasEntries(session.pendingUserDialogs)) {
32
+ return "This session is waiting for a permission or user-input response.";
33
+ }
34
+ if (workerProposalControl.pendingProposal(session)) return "A Split Worker proposal is still waiting for a decision.";
35
+ var pair = continuationPair.inspect(session, ctx);
36
+ if (!pair.ok) return pair.error;
37
+ if (!pair.group && (session._pairDelegation || continuationPair.activeFollowups(session))) {
38
+ return "Split Worker work or follow-ups are still pending.";
39
+ }
40
+ if (session.pendingPush && session.pendingPush.length) return "Messages are still waiting for delivery in this session.";
41
+ if (session.taskStopRequested || session.destroying || session._runtimeRefreshRequested) {
42
+ return "This session is changing state; retry when it is idle.";
43
+ }
44
+ return "";
45
+ }
46
+
47
+ function measuredEvidence(session) {
48
+ var status = pairUsage.contextStatus(session);
49
+ var current = status.current || {};
50
+ if (current.scope === "current_context" && current.usedRatio !== null &&
51
+ current.usedRatio >= PRESSURE_RATIO && current.usedTokens >= 0 && current.windowTokens > 0) {
52
+ return { status: status, evidence: {
53
+ kind: "current_context_pressure",
54
+ source: current.source,
55
+ usedTokens: current.usedTokens,
56
+ windowTokens: current.windowTokens,
57
+ usedRatio: current.usedRatio,
58
+ thresholdRatio: PRESSURE_RATIO,
59
+ } };
60
+ }
61
+ var compactions = status.compactions || {};
62
+ if (compactions.observedCount > 0 && compactions.active === false) {
63
+ return { status: status, evidence: {
64
+ kind: "recorded_compaction",
65
+ observedCount: compactions.observedCount,
66
+ lastObservedAt: compactions.lastObservedAt || null,
67
+ source: compactions.scope,
68
+ } };
69
+ }
70
+ return { status: status, error: "Continuation needs measured current-context pressure or a completed compaction recorded by Clay." };
71
+ }
72
+
73
+ function unsupportedSavings(value) {
74
+ return /guarantee(?:d|s)?\s+(token|context|cost|time|efficien|saving)|will\s+save\s+\d|save\s+\d+\s*%/i.test(value || "");
75
+ }
76
+
77
+ function evaluate(session, ctx, fields) {
78
+ var previous = historicalProposal(session);
79
+ if (previous) return { error: "This source session has already posted its one proactive continuation proposal.", previous: previous };
80
+ if (session.driverContinuationDecline) return { error: "The user chose to stay in this source session. Do not propose continuation again." };
81
+ var unsafe = unsafeReason(session, ctx);
82
+ if (unsafe) return { error: unsafe };
83
+ if (!fields.reason || fields.reason.length < MIN_JUSTIFICATION_CHARS) {
84
+ return { error: "Explain why moving now is useful with a concrete reason." };
85
+ }
86
+ if (!fields.milestone || fields.milestone.length < MIN_JUSTIFICATION_CHARS) {
87
+ return { error: "Describe the completed milestone or safe boundary before proposing continuation." };
88
+ }
89
+ if (!fields.benefit || fields.benefit.length < MIN_JUSTIFICATION_CHARS) {
90
+ return { error: "Describe the concrete continuity benefit of moving now." };
91
+ }
92
+ if (!fields.nextAction || fields.nextAction.length < MIN_JUSTIFICATION_CHARS) {
93
+ return { error: "Provide a prepared, concrete next action for the successor." };
94
+ }
95
+ if (unsupportedSavings(fields.reason + "\n" + fields.benefit)) {
96
+ return { error: "Do not claim guaranteed or quantified savings without measured evidence." };
97
+ }
98
+ return measuredEvidence(session);
99
+ }
100
+
101
+ function prompt(session) {
102
+ var observed = measuredEvidence(session);
103
+ var evidence = observed.evidence ? JSON.stringify(observed.evidence) : "not currently eligible";
104
+ return "A proactive Driver continuation is rare and may be proposed at most once for this source session. " +
105
+ "The server currently reports: " + evidence + ". Propose only after a concrete milestone at a safe boundary, with a specific reason to move now, a continuity benefit, and a prepared next action. " +
106
+ "Clay validates current-context pressure or a completed recorded compaction; cumulative spend, unknown status, conversation length, and model-supplied evidence do not qualify. Never promise token, time, cost, or efficiency savings. Stay here permanently suppresses proactive continuation for this source. Human acceptance is always required.";
107
+ }
108
+
109
+ module.exports = { PRESSURE_RATIO: PRESSURE_RATIO, historicalProposal: historicalProposal,
110
+ measuredEvidence: measuredEvidence, evaluate: evaluate, prompt: prompt };