clay-server 4.1.0-beta.4 → 4.1.0-beta.6
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/autonomous-run-record.js +32 -0
- package/lib/autonomous-run-scheduler.js +92 -0
- package/lib/durable-scheduler-record.js +61 -0
- package/lib/durable-scheduler-store.js +185 -0
- package/lib/durable-scheduler.js +495 -0
- package/lib/loop-cron.js +47 -0
- package/lib/loop-durable-schedule.js +159 -0
- package/lib/loop-guidance.js +35 -0
- package/lib/loop-registry-occurrence.js +76 -0
- package/lib/project-autonomous-run.js +498 -0
- package/lib/project-connection.js +4 -1
- package/lib/project-full-access.js +86 -0
- package/lib/project-http.js +3 -1
- package/lib/project-loop-interview.js +163 -0
- package/lib/project-loop.js +150 -64
- package/lib/project-pair-autonomous-stop.js +16 -0
- package/lib/project-pair-owned-stop.js +18 -0
- package/lib/project-pair-task-control.js +14 -1
- package/lib/project-scheduled-messages.js +316 -0
- package/lib/project-scheduled-task-execution.js +236 -0
- package/lib/project-scheduled-tasks.js +483 -0
- package/lib/project-session-pair.js +16 -19
- package/lib/project-sessions.js +60 -28
- package/lib/project-user-message.js +24 -18
- package/lib/project.js +260 -52
- package/lib/public/app.js +60 -23
- package/lib/public/css/home-hub.css +15 -2
- package/lib/public/css/home-sidebar.css +50 -2
- package/lib/public/css/input.css +71 -0
- package/lib/public/css/mates.css +0 -1
- package/lib/public/css/pane.css +58 -7
- package/lib/public/css/rewind.css +11 -2
- package/lib/public/css/scheduled-tasks.css +123 -0
- package/lib/public/css/sticky-notes.css +24 -4
- package/lib/public/index.html +19 -134
- package/lib/public/modules/app-loop-ui.js +4 -8
- package/lib/public/modules/app-messages.js +22 -6
- package/lib/public/modules/app-projects.js +2 -0
- package/lib/public/modules/app-rate-limit.js +8 -5
- package/lib/public/modules/autonomous-run.js +285 -0
- package/lib/public/modules/home-sidebar.js +16 -8
- package/lib/public/modules/home-surface.js +5 -0
- package/lib/public/modules/input.js +5 -4
- package/lib/public/modules/loop-interview.js +151 -0
- package/lib/public/modules/project-logs.js +2 -2
- package/lib/public/modules/scheduled-message-state.js +46 -0
- package/lib/public/modules/scheduled-task-edit-state.js +7 -0
- package/lib/public/modules/scheduled-task-engine.js +98 -0
- package/lib/public/modules/scheduled-task-form.js +122 -0
- package/lib/public/modules/scheduled-task-recurrence.js +46 -0
- package/lib/public/modules/scheduled-task-schedule-rule.js +115 -0
- package/lib/public/modules/scheduled-tasks.js +468 -0
- package/lib/public/modules/scheduler-config.js +6 -52
- package/lib/public/modules/scheduler.js +9 -21
- package/lib/public/modules/sidebar.js +0 -11
- package/lib/public/modules/split-view.js +2 -0
- package/lib/public/modules/sticky-notes-browser.js +1 -1
- package/lib/public/modules/sticky-notes.js +117 -4
- package/lib/public/modules/tool-palette-order.js +2 -3
- package/lib/public/modules/tools.js +5 -2
- package/lib/public/style.css +1 -0
- package/lib/record-revision.js +11 -0
- package/lib/schedule-rule.js +151 -0
- package/lib/schedule-validation.js +124 -0
- package/lib/scheduled-run-state.js +87 -0
- package/lib/scheduled-task-records.js +127 -0
- package/lib/scheduler.js +179 -196
- package/lib/sdk-bridge.js +123 -11
- package/lib/sdk-message-processor.js +3 -0
- package/lib/server-durable-scheduler.js +26 -0
- package/lib/server.js +22 -0
- package/lib/session-pair-factory.js +42 -4
- package/lib/session-pair-query-start.js +22 -0
- package/lib/session-query-tool-bridge.js +52 -0
- package/lib/session-split-groups.js +35 -1
- package/lib/sessions.js +51 -3
- package/lib/users-home-surface-preferences.js +2 -1
- package/lib/users-scheduled-task-preferences.js +44 -0
- package/lib/users.js +6 -0
- package/lib/ws-schema.js +25 -0
- package/lib/yoke/adapters/antigravity.js +1 -0
- package/lib/yoke/adapters/claude-worker.js +7 -4
- package/lib/yoke/adapters/claude.js +31 -1
- package/lib/yoke/adapters/codex.js +3 -0
- package/lib/yoke/claude-user-input.js +2 -0
- package/lib/yoke/index.js +2 -0
- package/lib/yoke/mcp-bridge-server.js +7 -1
- package/lib/yoke/session-tool-transport.js +53 -0
- package/lib/yoke/user-input.js +21 -7
- package/package.json +1 -1
- package/lib/public/modules/app-loop-wizard.js +0 -439
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Shared, transactional control for Clay-managed per-session Skip Permissions.
|
|
2
|
+
|
|
3
|
+
function createFullAccessService(ctx) {
|
|
4
|
+
var sm = ctx.sm;
|
|
5
|
+
var transitions = new Map();
|
|
6
|
+
|
|
7
|
+
function serialize(session, action) {
|
|
8
|
+
var prior = transitions.get(session.localId) || Promise.resolve();
|
|
9
|
+
var next = prior.catch(function () {}).then(action);
|
|
10
|
+
transitions.set(session.localId, next);
|
|
11
|
+
return next.finally(function () {
|
|
12
|
+
if (transitions.get(session.localId) === next) transitions.delete(session.localId);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function snapshot(session) {
|
|
17
|
+
return {
|
|
18
|
+
permissionMode: session.permissionMode || null,
|
|
19
|
+
permissionModeBeforeFullAccess: session.permissionModeBeforeFullAccess || null,
|
|
20
|
+
enabled: session.permissionMode === "bypassPermissions",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function notify(session) {
|
|
25
|
+
if (typeof sm.sendToSession === "function") sm.sendToSession(session, { type: "session_full_access_changed", id: session.localId,
|
|
26
|
+
enabled: session.permissionMode === "bypassPermissions", permissionMode: session.permissionMode });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function applyNativeMode(session, enabled) {
|
|
30
|
+
if (!session.queryInstance || typeof session.queryInstance.setPermissionMode !== "function") return;
|
|
31
|
+
// Clay's common permission callback remains the approval authority.
|
|
32
|
+
await session.queryInstance.setPermissionMode(enabled ? "default" : session.permissionMode);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function applyEnabled(session, enabled) {
|
|
36
|
+
var previous = snapshot(session);
|
|
37
|
+
if (previous.enabled === !!enabled) return previous;
|
|
38
|
+
if (enabled) {
|
|
39
|
+
session.permissionModeBeforeFullAccess = session.permissionMode || sm.currentPermissionMode || "default";
|
|
40
|
+
session.permissionMode = "bypassPermissions";
|
|
41
|
+
} else {
|
|
42
|
+
session.permissionMode = session.permissionModeBeforeFullAccess || "default";
|
|
43
|
+
session.permissionModeBeforeFullAccess = null;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
await applyNativeMode(session, !!enabled);
|
|
47
|
+
sm.saveSessionFile(session);
|
|
48
|
+
sm.broadcastSessionList();
|
|
49
|
+
notify(session);
|
|
50
|
+
return previous;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
session.permissionMode = previous.permissionMode;
|
|
53
|
+
session.permissionModeBeforeFullAccess = previous.permissionModeBeforeFullAccess;
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function setEnabled(session, enabled) {
|
|
59
|
+
return serialize(session, function () { return applyEnabled(session, enabled); });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function applyRestore(session, prior) {
|
|
63
|
+
if (!prior) return;
|
|
64
|
+
var current = snapshot(session);
|
|
65
|
+
session.permissionMode = prior.permissionMode;
|
|
66
|
+
session.permissionModeBeforeFullAccess = prior.permissionModeBeforeFullAccess;
|
|
67
|
+
try {
|
|
68
|
+
await applyNativeMode(session, prior.enabled);
|
|
69
|
+
sm.saveSessionFile(session);
|
|
70
|
+
sm.broadcastSessionList();
|
|
71
|
+
notify(session);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
session.permissionMode = current.permissionMode;
|
|
74
|
+
session.permissionModeBeforeFullAccess = current.permissionModeBeforeFullAccess;
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function restore(session, prior) {
|
|
80
|
+
return serialize(session, function () { return applyRestore(session, prior); });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { restore: restore, setEnabled: setEnabled, snapshot: snapshot };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { createFullAccessService: createFullAccessService };
|
package/lib/project-http.js
CHANGED
|
@@ -898,7 +898,9 @@ function attachHTTP(ctx) {
|
|
|
898
898
|
}
|
|
899
899
|
var sessionId = Number(body.sessionId);
|
|
900
900
|
if (!Number.isInteger(sessionId)) sessionId = null;
|
|
901
|
-
var
|
|
901
|
+
var queryGeneration = Number(body.queryGeneration);
|
|
902
|
+
if (!Number.isInteger(queryGeneration)) queryGeneration = null;
|
|
903
|
+
var handler = getMcpBridgeHandler(sessionId, body.sessionOnly === true, queryGeneration);
|
|
902
904
|
if (!handler) {
|
|
903
905
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
904
906
|
res.end('{"error":"MCP bridge handler unavailable"}');
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
var crypto = require("crypto");
|
|
2
|
+
var driverEligibility = require("./session-driver-eligibility");
|
|
3
|
+
var loopGuidance = require("./loop-guidance");
|
|
4
|
+
|
|
5
|
+
var MAX_OBJECTIVE = 20000;
|
|
6
|
+
var MAX_CRITERIA = 10;
|
|
7
|
+
var MAX_CRITERION = 1000;
|
|
8
|
+
|
|
9
|
+
function text(value, max) { return typeof value === "string" ? value.trim().substring(0, max) : ""; }
|
|
10
|
+
function criteria(value) { return (Array.isArray(value) ? value : []).slice(0, MAX_CRITERIA).map(function (item) { return text(item, MAX_CRITERION); }).filter(Boolean); }
|
|
11
|
+
function activeInterviewId(session) {
|
|
12
|
+
var history = Array.isArray(session && session.history) ? session.history : [];
|
|
13
|
+
var current = null;
|
|
14
|
+
for (var i = 0; i < history.length; i++) {
|
|
15
|
+
var entry = history[i];
|
|
16
|
+
if (entry && entry.source === "loop_interview") current = entry.interviewId || null;
|
|
17
|
+
if (entry && (entry.source === "loop_interview_cancel" || entry.source === "loop_interview_close") && entry.interviewId === current) current = null;
|
|
18
|
+
}
|
|
19
|
+
return current;
|
|
20
|
+
}
|
|
21
|
+
function brief(value) {
|
|
22
|
+
if (!value || typeof value !== "object") return null;
|
|
23
|
+
var objective = text(value.objective, MAX_OBJECTIVE);
|
|
24
|
+
var successCriteria = criteria(value.successCriteria);
|
|
25
|
+
if (!objective || successCriteria.length === 0) return null;
|
|
26
|
+
return { version: 1, id: text(value.id, 200) || crypto.randomUUID(), objective: objective,
|
|
27
|
+
successCriteria: successCriteria, maxContinuations: Math.max(1, Math.min(100, Math.floor(Number(value.maxContinuations) || 10))),
|
|
28
|
+
maxMinutes: Math.max(1, Math.min(1440, Math.floor(Number(value.maxMinutes) || 60))), createdAt: Number(value.createdAt) || Date.now() };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function attachLoopInterview(ctx) {
|
|
32
|
+
var sm = ctx.sm;
|
|
33
|
+
var sendTo = ctx.sendTo;
|
|
34
|
+
var autonomousRun = ctx.autonomousRun;
|
|
35
|
+
function eligible(session) { return !!(session && !ctx.isMate && (session.runtimeMode || session.mode) === "gui" && driverEligibility.isEligibleDriverSession(session) && !ctx.isDriverOperatedSession(session)); }
|
|
36
|
+
function runActive(session) { return !!(session && session.autonomousRun && ["armed", "running", "reviewing", "waiting-worker", "waiting-user", "paused"].indexOf(session.autonomousRun.state) !== -1); }
|
|
37
|
+
function hasInterview(session, requestId) {
|
|
38
|
+
var history = Array.isArray(session && session.history) ? session.history : [];
|
|
39
|
+
for (var i = history.length - 1; i >= 0; i--) if (history[i] && history[i].source === "loop_interview" && history[i].requestId === requestId) return true;
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
function currentInterview(session) {
|
|
43
|
+
return activeInterviewId(session);
|
|
44
|
+
}
|
|
45
|
+
function interviewActive(session) {
|
|
46
|
+
return !!currentInterview(session);
|
|
47
|
+
}
|
|
48
|
+
function startInterview(ws, msg) {
|
|
49
|
+
var session = ctx.getSessionForWs(ws);
|
|
50
|
+
var requestId = text(msg.requestId, 200);
|
|
51
|
+
var result = { type: "loop_interview_start_result", sessionId: session && session.localId || msg.sessionId || null, requestId: requestId, ok: false };
|
|
52
|
+
if (!session || !requestId || Number(msg.sessionId) !== Number(session.localId) || (ctx.canAccess && !ctx.canAccess(ws, session))) result.error = "This session is no longer available.";
|
|
53
|
+
else if (hasInterview(session, requestId)) { result.ok = true; result.duplicate = true; }
|
|
54
|
+
else if (!eligible(session)) result.error = "Loop interviews are available only in a Driver project chat.";
|
|
55
|
+
else if (runActive(session)) result.error = "Finish the active Loop run before starting another interview.";
|
|
56
|
+
else if (session.isProcessing || session._queryStarting) result.error = "Wait for the current task to finish before starting a Loop interview.";
|
|
57
|
+
else {
|
|
58
|
+
session.loopInterviewBrief = null;
|
|
59
|
+
var prompt = loopGuidance.interviewPrompt();
|
|
60
|
+
var interviewId = crypto.randomUUID();
|
|
61
|
+
var entry = { type: "user_message", text: "[Loop interview started]", source: "loop_interview", interviewId: interviewId, requestId: requestId, _ts: Date.now() };
|
|
62
|
+
session.history.push(entry);
|
|
63
|
+
sm.appendToSessionFile(session, entry);
|
|
64
|
+
sm.saveSessionFile(session);
|
|
65
|
+
publish(session);
|
|
66
|
+
ctx.sendToSession(session.localId, entry);
|
|
67
|
+
session.isProcessing = true;
|
|
68
|
+
ctx.onProcessingChanged();
|
|
69
|
+
ctx.sendToSession(session.localId, { type: "status", status: "processing" });
|
|
70
|
+
try {
|
|
71
|
+
var started = ctx.sdk.startQuery(session, prompt, null, ctx.ensureProjectAccessForSession(session));
|
|
72
|
+
ctx.sendTo(ws, Object.assign({}, result, { ok: true }));
|
|
73
|
+
Promise.resolve(started).catch(function (error) {
|
|
74
|
+
var stillCurrent = ctx.getSessionForWs(ws) === session && currentInterview(session) === interviewId && hasInterview(session, requestId);
|
|
75
|
+
if (!stillCurrent) return;
|
|
76
|
+
session.isProcessing = false;
|
|
77
|
+
ctx.onProcessingChanged();
|
|
78
|
+
ctx.sendToSession(session.localId, { type: "status", status: "idle" });
|
|
79
|
+
sm.saveSessionFile(session);
|
|
80
|
+
ctx.sendTo(ws, Object.assign({}, result, { ok: false, error: "Loop interview could not start: " + (error.message || error) }));
|
|
81
|
+
});
|
|
82
|
+
return true;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
session.isProcessing = false;
|
|
85
|
+
ctx.onProcessingChanged();
|
|
86
|
+
ctx.sendToSession(session.localId, { type: "status", status: "idle" });
|
|
87
|
+
sm.saveSessionFile(session);
|
|
88
|
+
result.error = "Loop interview could not start: " + (error.message || error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
ctx.sendTo(ws, result);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
function publish(session) { sm.sendToSession(session, { type: "loop_interview_brief", sessionId: session.localId, brief: session.loopInterviewBrief || null }); }
|
|
95
|
+
function propose(session, interviewId, args) {
|
|
96
|
+
if (!eligible(session) || runActive(session) || !interviewId || currentInterview(session) !== interviewId) return Promise.resolve({ content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: "propose_loop is stale or unavailable outside the current Driver Loop interview." }) }] });
|
|
97
|
+
var next = brief(args);
|
|
98
|
+
if (!next) return Promise.resolve({ content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: "Provide an objective and at least one success criterion." }) }] });
|
|
99
|
+
next.interviewId = interviewId;
|
|
100
|
+
session.loopInterviewBrief = next;
|
|
101
|
+
sm.saveSessionFile(session);
|
|
102
|
+
publish(session);
|
|
103
|
+
return Promise.resolve({ content: [{ type: "text", text: JSON.stringify({ status: "proposed", brief: next }) }] });
|
|
104
|
+
}
|
|
105
|
+
function getToolDefs(session) {
|
|
106
|
+
if (!eligible(session) || runActive(session) || !interviewActive(session) || session.loopInterviewBrief) return [];
|
|
107
|
+
var interviewId = currentInterview(session);
|
|
108
|
+
return [{ name: "propose_loop", description: "Draft a bounded Loop execution brief for human review. This never starts execution or changes permissions.", inputSchema: { type: "object", properties: {
|
|
109
|
+
objective: { type: "string" }, successCriteria: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 10 },
|
|
110
|
+
maxContinuations: { type: "integer", minimum: 1, maximum: 100 }, maxMinutes: { type: "integer", minimum: 1, maximum: 1440 }
|
|
111
|
+
}, required: ["objective", "successCriteria"], additionalProperties: false }, handler: function (args) { return propose(session, interviewId, args || {}); } }];
|
|
112
|
+
}
|
|
113
|
+
function handleMessage(ws, msg) {
|
|
114
|
+
if (msg.type === "loop_interview_state") {
|
|
115
|
+
var stateSession = ctx.getSessionForWs(ws);
|
|
116
|
+
if (stateSession && Number(msg.sessionId) === Number(stateSession.localId) && (!ctx.canAccess || ctx.canAccess(ws, stateSession))) publish(stateSession);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
if (msg.type === "loop_interview_start") return startInterview(ws, msg);
|
|
120
|
+
if (["loop_interview_start_run", "loop_interview_edit", "loop_interview_cancel"].indexOf(msg.type) === -1) return false;
|
|
121
|
+
var session = ctx.getSessionForWs(ws);
|
|
122
|
+
if (!session || Number(msg.sessionId) !== Number(session.localId) || (ctx.canAccess && !ctx.canAccess(ws, session)) || !eligible(session)) {
|
|
123
|
+
if (msg.type === "loop_interview_start_run") sendTo(ws, { type: "loop_interview_start_run_result", ok: false, sessionId: session && session.localId || msg.sessionId || null, requestId: text(msg.requestId, 200), error: "This session is no longer available." });
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (msg.type === "loop_interview_cancel") {
|
|
127
|
+
if (session.loopInterviewBrief && (text(msg.proposalId, 200) !== session.loopInterviewBrief.id || Number(msg.version) !== Number(session.loopInterviewBrief.version))) return true;
|
|
128
|
+
var cancelEntry = { type: "loop_interview_cancel", source: "loop_interview_cancel", interviewId: currentInterview(session), _ts: Date.now() };
|
|
129
|
+
session.history.push(cancelEntry); sm.appendToSessionFile(session, cancelEntry);
|
|
130
|
+
session.loopInterviewBrief = null; sm.saveSessionFile(session); publish(session); return true;
|
|
131
|
+
}
|
|
132
|
+
if (msg.type === "loop_interview_edit") {
|
|
133
|
+
if (session.loopInterviewBrief && text(msg.proposalId, 200) === session.loopInterviewBrief.id && Number(msg.version) === Number(session.loopInterviewBrief.version) && session.loopInterviewBrief.interviewId === currentInterview(session)) {
|
|
134
|
+
var edited = brief(Object.assign({}, session.loopInterviewBrief, msg.brief || {}));
|
|
135
|
+
if (edited) { edited.interviewId = session.loopInterviewBrief.interviewId; edited.version = session.loopInterviewBrief.version + 1; session.loopInterviewBrief = edited; sm.saveSessionFile(session); publish(session); }
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
var proposal = session.loopInterviewBrief;
|
|
140
|
+
if (!proposal || proposal.interviewId !== currentInterview(session) || text(msg.proposalId, 200) !== proposal.id || Number(msg.version) !== Number(proposal.version)) {
|
|
141
|
+
sendTo(ws, { type: "loop_interview_start_run_result", ok: false, sessionId: session.localId, requestId: text(msg.requestId, 200), error: "This Loop brief is stale or no longer pending." });
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
autonomousRun.startApprovedBrief(ws, session, proposal, text(msg.requestId, 200), function () {
|
|
145
|
+
return !!(session.loopInterviewBrief && session.loopInterviewBrief.id === proposal.id && Number(session.loopInterviewBrief.version) === Number(proposal.version) && session.loopInterviewBrief.interviewId === currentInterview(session));
|
|
146
|
+
}).then(function (ok) {
|
|
147
|
+
if (ok && session.loopInterviewBrief && session.loopInterviewBrief.id === proposal.id) {
|
|
148
|
+
var closeEntry = { type: "loop_interview_close", source: "loop_interview_close", interviewId: proposal.interviewId, proposalId: proposal.id, proposalVersion: proposal.version, _ts: Date.now() };
|
|
149
|
+
session.history.push(closeEntry);
|
|
150
|
+
sm.appendToSessionFile(session, closeEntry);
|
|
151
|
+
session.loopInterviewBrief = null;
|
|
152
|
+
sm.saveSessionFile(session);
|
|
153
|
+
publish(session);
|
|
154
|
+
}
|
|
155
|
+
}).catch(function (error) {
|
|
156
|
+
sendTo(ws, { type: "loop_interview_start_run_result", ok: false, sessionId: session.localId, requestId: text(msg.requestId, 200), error: error.message || String(error) });
|
|
157
|
+
});
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
return { getToolDefs: getToolDefs, handleMessage: handleMessage };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = { attachLoopInterview: attachLoopInterview, brief: brief, activeInterviewId: activeInterviewId };
|
package/lib/project-loop.js
CHANGED
|
@@ -3,6 +3,10 @@ var path = require("path");
|
|
|
3
3
|
var crypto = require("crypto");
|
|
4
4
|
var { execFileSync } = require("child_process");
|
|
5
5
|
var { createLoopRegistry } = require("./scheduler");
|
|
6
|
+
var loopGuidance = require("./loop-guidance");
|
|
7
|
+
var validateCron = require("./schedule-validation").validateCron;
|
|
8
|
+
var validateScheduleRule = require("./schedule-rule").validateScheduleRule;
|
|
9
|
+
var attachLoopDurableSchedule = require("./loop-durable-schedule").attachLoopDurableSchedule;
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* Attach loop engine to a project context.
|
|
@@ -62,7 +66,7 @@ function attachLoop(ctx) {
|
|
|
62
66
|
var _loopUtils = require("./utils");
|
|
63
67
|
var _loopDir = path.join(_loopConfig.CONFIG_DIR, "loops");
|
|
64
68
|
var _loopEncodedCwd = _loopUtils.resolveEncodedFile(_loopDir, cwd, ".json");
|
|
65
|
-
var _loopStatePath = path.join(_loopDir, _loopEncodedCwd + ".json");
|
|
69
|
+
var _loopStatePath = ctx.loopStatePath || path.join(_loopDir, _loopEncodedCwd + ".json");
|
|
66
70
|
|
|
67
71
|
function saveLoopState() {
|
|
68
72
|
try {
|
|
@@ -269,18 +273,43 @@ function attachLoop(ctx) {
|
|
|
269
273
|
|
|
270
274
|
// --- Loop Registry (unified one-off + scheduled) ---
|
|
271
275
|
var activeRegistryId = null; // track which registry record triggered current loop
|
|
272
|
-
var
|
|
276
|
+
var durableSchedule = null;
|
|
277
|
+
function isMultiUser() { return typeof ctx.isMultiUser === "function" ? ctx.isMultiUser() : !!ctx.isMultiUser; }
|
|
278
|
+
|
|
279
|
+
function legacyRuntimeFor(session) {
|
|
280
|
+
var record = loopRegistry.getById(loopState.loopId);
|
|
281
|
+
var linuxUser = getLinuxUserForSession(session);
|
|
282
|
+
if (!isMultiUser()) return { ok: true, linuxUser: linuxUser };
|
|
283
|
+
if (!record || !record.ownerId || session.ownerId !== record.ownerId) return { ok: false, error: "The scheduled Loop owner changed before execution." };
|
|
284
|
+
if (ctx.authorizeScheduledRun && !ctx.authorizeScheduledRun(record)) return { ok: false, error: "The scheduled Loop owner is no longer authorized for this project." };
|
|
285
|
+
if (ctx.requiresLinuxUser && ctx.requiresLinuxUser() && !linuxUser) return { ok: false, error: "The scheduled Loop owner no longer has a valid OS identity." };
|
|
286
|
+
return { ok: true, linuxUser: linuxUser };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function rejectLegacyRuntime(session, error) {
|
|
290
|
+
var message = error || "The scheduled Loop execution identity is no longer valid.";
|
|
291
|
+
var entry = { type: "error", text: message };
|
|
292
|
+
session.isProcessing = false; session.singleTurn = false; if (session.loop) session.loop.active = false;
|
|
293
|
+
session.history.push(entry); sm.appendToSessionFile(session, entry); sm.saveSessionFile(session);
|
|
294
|
+
sendToSession(session.localId, entry); sendToSession(session.localId, { type: "status", status: "idle" }); send({ type: "loop_error", text: message }); finishLoop("error");
|
|
295
|
+
}
|
|
273
296
|
|
|
274
297
|
function triggerFromQueue(record) {
|
|
275
|
-
|
|
298
|
+
if (record.createdViaScheduledTasks && ctx.authorizeScheduledRun && !ctx.authorizeScheduledRun(record)) {
|
|
299
|
+
console.error("[loop-registry] Scheduled task owner is no longer authorized: " + record.name);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
// Linked calendar schedules use their task's files. Standalone Scheduled Tasks own theirs.
|
|
276
303
|
var loopFilesId = record.id;
|
|
277
304
|
if (record.source === "schedule") {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
305
|
+
var standaloneScheduledTask = record.createdViaScheduledTasks && record.mode === "simple" && !record.linkedTaskId;
|
|
306
|
+
if (!record.linkedTaskId && !standaloneScheduledTask) {
|
|
307
|
+
failScheduledDispatch("Schedule has no linked task: " + record.name);
|
|
308
|
+
}
|
|
309
|
+
if (record.linkedTaskId) {
|
|
310
|
+
loopFilesId = record.linkedTaskId;
|
|
311
|
+
console.log("[loop-registry] Schedule triggered: " + record.name + " -> linked task " + loopFilesId);
|
|
281
312
|
}
|
|
282
|
-
loopFilesId = record.linkedTaskId;
|
|
283
|
-
console.log("[loop-registry] Schedule triggered: " + record.name + " -> linked task " + loopFilesId);
|
|
284
313
|
}
|
|
285
314
|
|
|
286
315
|
// Verify the loop directory and PROMPT.md exist
|
|
@@ -288,8 +317,7 @@ function attachLoop(ctx) {
|
|
|
288
317
|
try {
|
|
289
318
|
fs.accessSync(path.join(recDir, "PROMPT.md"));
|
|
290
319
|
} catch (e) {
|
|
291
|
-
|
|
292
|
-
return;
|
|
320
|
+
failScheduledDispatch("PROMPT.md is missing for " + loopFilesId + ".");
|
|
293
321
|
}
|
|
294
322
|
// Set the loopId to the schedule's own id (not the linked task) so sidebar groups correctly
|
|
295
323
|
loopState.loopId = record.id;
|
|
@@ -304,28 +332,65 @@ function attachLoop(ctx) {
|
|
|
304
332
|
startLoop({ maxIterations: record.maxIterations, name: record.name });
|
|
305
333
|
}
|
|
306
334
|
|
|
335
|
+
function failScheduledDispatch(message) {
|
|
336
|
+
console.error("[loop-registry] " + message);
|
|
337
|
+
send({ type: "loop_error", text: message });
|
|
338
|
+
var error = new Error(message); error.code = "SCHEDULE_DISPATCH_FAILED"; throw error;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function usesPairExecution(record) { return !!(record && (record.createdViaScheduledTasks === true || record.source === "schedule")); }
|
|
342
|
+
|
|
307
343
|
var loopRegistry = createLoopRegistry({
|
|
308
344
|
cwd: cwd,
|
|
309
|
-
|
|
310
|
-
|
|
345
|
+
registryPath: ctx.loopRegistryPath,
|
|
346
|
+
canTrigger: function (record) {
|
|
347
|
+
if (!usesPairExecution(record)) return true;
|
|
348
|
+
return ctx.canExecuteScheduledTask ? ctx.canExecuteScheduledTask(record) : false;
|
|
349
|
+
},
|
|
350
|
+
prepareTrigger: function (record) {
|
|
351
|
+
if (!usesPairExecution(record)) return null;
|
|
352
|
+
return ctx.prepareScheduledTask ? ctx.prepareScheduledTask(record, "schedule") : { ok: false };
|
|
353
|
+
},
|
|
354
|
+
onTrigger: function (record, prepared) {
|
|
355
|
+
if (usesPairExecution(record)) {
|
|
356
|
+
if (prepared && (prepared.skipped || prepared.queued)) return;
|
|
357
|
+
var scheduledResult = ctx.executeScheduledTask && ctx.executeScheduledTask(record, "schedule", prepared);
|
|
358
|
+
if (!scheduledResult || !scheduledResult.ok) failScheduledDispatch("Pair execution did not start: " + (scheduledResult && scheduledResult.error || record.name));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
// A busy non-skipping occurrence remains queued in the durable scheduler.
|
|
311
362
|
if (loopState.active || loopState.phase === "executing") {
|
|
312
363
|
if (record.skipIfRunning !== false) {
|
|
313
364
|
console.log("[loop-registry] Skipping trigger for " + record.name + " — loop already active (skipIfRunning)");
|
|
314
365
|
return;
|
|
315
366
|
}
|
|
316
|
-
console.log("[loop-registry] Loop active, queuing trigger for " + record.name);
|
|
317
|
-
pendingTriggers.push(record);
|
|
318
367
|
return;
|
|
319
368
|
}
|
|
320
369
|
|
|
321
370
|
triggerFromQueue(record);
|
|
322
371
|
},
|
|
323
372
|
onChange: function () {
|
|
373
|
+
if (durableSchedule) durableSchedule.sync();
|
|
324
374
|
send({ type: "loop_registry_updated", records: getHubSchedules() });
|
|
325
375
|
},
|
|
326
376
|
});
|
|
327
377
|
loopRegistry.load();
|
|
328
|
-
|
|
378
|
+
if (ctx.durableScheduler) {
|
|
379
|
+
durableSchedule = attachLoopDurableSchedule({
|
|
380
|
+
scheduler: ctx.durableScheduler,
|
|
381
|
+
registry: loopRegistry,
|
|
382
|
+
projectId: slug,
|
|
383
|
+
isMultiUser: ctx.isMultiUser,
|
|
384
|
+
authorize: function (record) {
|
|
385
|
+
if (!isMultiUser() && !usesPairExecution(record)) return true;
|
|
386
|
+
return !ctx.authorizeScheduledRun || ctx.authorizeScheduledRun(record);
|
|
387
|
+
},
|
|
388
|
+
canRun: function (record) {
|
|
389
|
+
if (usesPairExecution(record)) return ctx.canExecuteScheduledTask ? ctx.canExecuteScheduledTask(record) : false;
|
|
390
|
+
return !(record.skipIfRunning === false && (loopState.active || loopState.phase === "executing"));
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
} else loopRegistry.startTimer();
|
|
329
394
|
|
|
330
395
|
// Wire loop info resolution for session list broadcasts
|
|
331
396
|
sm.setResolveLoopInfo(function (loopId) {
|
|
@@ -412,8 +477,8 @@ function attachLoop(ctx) {
|
|
|
412
477
|
return;
|
|
413
478
|
}
|
|
414
479
|
|
|
415
|
-
var session = sm.createSession();
|
|
416
480
|
var loopSource = loopRegistry.getById(loopState.loopId);
|
|
481
|
+
var session = sm.createSession({ ownerId: loopSource && loopSource.ownerId || null });
|
|
417
482
|
var loopName = (loopState.wizardData && loopState.wizardData.name) || (loopSource && loopSource.name) || "";
|
|
418
483
|
var loopSourceTag = (loopSource && loopSource.source) || null;
|
|
419
484
|
var isRalphLoop = loopSourceTag === "ralph";
|
|
@@ -430,6 +495,8 @@ function attachLoop(ctx) {
|
|
|
430
495
|
maxIterations: loopState.maxIterations,
|
|
431
496
|
sessionId: session.localId,
|
|
432
497
|
});
|
|
498
|
+
var coderRuntime = legacyRuntimeFor(session);
|
|
499
|
+
if (!coderRuntime.ok) { rejectLegacyRuntime(session, coderRuntime.error); return; }
|
|
433
500
|
|
|
434
501
|
var coderCompleted = false;
|
|
435
502
|
session.onQueryComplete = function(completedSession) {
|
|
@@ -508,7 +575,7 @@ function attachLoop(ctx) {
|
|
|
508
575
|
session.acceptEditsAfterStart = true;
|
|
509
576
|
session.singleTurn = true;
|
|
510
577
|
if (loopState.settings) session.loopSettings = loopState.settings;
|
|
511
|
-
sdk.startQuery(session, loopState.promptText, undefined,
|
|
578
|
+
sdk.startQuery(session, loopState.promptText, undefined, coderRuntime.linuxUser);
|
|
512
579
|
}
|
|
513
580
|
|
|
514
581
|
function runJudge() {
|
|
@@ -549,8 +616,8 @@ function attachLoop(ctx) {
|
|
|
549
616
|
"- PASS: [brief explanation]\n" +
|
|
550
617
|
"- FAIL: [brief explanation of what is still missing]";
|
|
551
618
|
|
|
552
|
-
var judgeSession = sm.createSession();
|
|
553
619
|
var judgeSource = loopRegistry.getById(loopState.loopId);
|
|
620
|
+
var judgeSession = sm.createSession({ ownerId: judgeSource && judgeSource.ownerId || null });
|
|
554
621
|
var judgeName = (loopState.wizardData && loopState.wizardData.name) || (judgeSource && judgeSource.name) || "";
|
|
555
622
|
var judgeSourceTag = (judgeSource && judgeSource.source) || null;
|
|
556
623
|
var isRalphJudge = judgeSourceTag === "ralph";
|
|
@@ -565,6 +632,8 @@ function attachLoop(ctx) {
|
|
|
565
632
|
iteration: loopState.iteration,
|
|
566
633
|
sessionId: judgeSession.localId,
|
|
567
634
|
});
|
|
635
|
+
var judgeRuntime = legacyRuntimeFor(judgeSession);
|
|
636
|
+
if (!judgeRuntime.ok) { rejectLegacyRuntime(judgeSession, judgeRuntime.error); return; }
|
|
568
637
|
|
|
569
638
|
var judgeCompleted = false;
|
|
570
639
|
judgeSession.onQueryComplete = function(completedSession) {
|
|
@@ -625,7 +694,7 @@ function attachLoop(ctx) {
|
|
|
625
694
|
judgeSession.acceptEditsAfterStart = true;
|
|
626
695
|
judgeSession.singleTurn = true;
|
|
627
696
|
if (loopState.settings) judgeSession.loopSettings = loopState.settings;
|
|
628
|
-
sdk.startQuery(judgeSession, judgePrompt, undefined,
|
|
697
|
+
sdk.startQuery(judgeSession, judgePrompt, undefined, judgeRuntime.linuxUser);
|
|
629
698
|
}
|
|
630
699
|
|
|
631
700
|
function parseJudgeVerdict(session) {
|
|
@@ -710,14 +779,7 @@ function attachLoop(ctx) {
|
|
|
710
779
|
});
|
|
711
780
|
}
|
|
712
781
|
|
|
713
|
-
|
|
714
|
-
if (pendingTriggers.length > 0) {
|
|
715
|
-
var next = pendingTriggers.shift();
|
|
716
|
-
console.log("[loop-registry] Processing queued trigger: " + next.name);
|
|
717
|
-
setTimeout(function () {
|
|
718
|
-
triggerFromQueue(next);
|
|
719
|
-
}, 1000);
|
|
720
|
-
}
|
|
782
|
+
if (durableSchedule) durableSchedule.wake();
|
|
721
783
|
}
|
|
722
784
|
|
|
723
785
|
function resumeLoop() {
|
|
@@ -891,13 +953,7 @@ function attachLoop(ctx) {
|
|
|
891
953
|
loopState.phase = "crafting";
|
|
892
954
|
saveLoopState();
|
|
893
955
|
|
|
894
|
-
var judgeCraftPrompt =
|
|
895
|
-
"The user has already provided PROMPT.md, so do NOT create or modify PROMPT.md. " +
|
|
896
|
-
"You MUST invoke the clay-ralph skill — do NOT execute the task yourself. " +
|
|
897
|
-
"Your job is to read the existing PROMPT.md and create a JUDGE.md " +
|
|
898
|
-
"that will evaluate whether the coder session completed the task successfully.\n\n" +
|
|
899
|
-
"## Task\n" + (wData.task || "") +
|
|
900
|
-
"\n\n## Loop Directory\n" + lDir;
|
|
956
|
+
var judgeCraftPrompt = loopGuidance.legacyCraftPrompt({ judgeOnly: true, task: wData.task, directory: lDir });
|
|
901
957
|
|
|
902
958
|
var judgeCraftSession = sm.createSession();
|
|
903
959
|
judgeCraftSession.title = (isRalphCraft ? "Ralph" : "Task") + (craftName ? " " + craftName : "") + " Crafting";
|
|
@@ -934,31 +990,9 @@ function attachLoop(ctx) {
|
|
|
934
990
|
return true;
|
|
935
991
|
}
|
|
936
992
|
|
|
937
|
-
// Default: "draft" mode — Clay crafts
|
|
993
|
+
// Default: "draft" mode — Clay crafts legacy PROMPT.md/JUDGE.md files
|
|
938
994
|
var _draftIsSimple = wData.loopMode === "simple";
|
|
939
|
-
var craftingPrompt;
|
|
940
|
-
if (_draftIsSimple) {
|
|
941
|
-
craftingPrompt = "Use the /clay-ralph skill to design a Ralph Loop for the following task. " +
|
|
942
|
-
"You MUST invoke the clay-ralph skill — do NOT execute the task yourself. " +
|
|
943
|
-
"This is a Simple Loop (no judge). Your job is to create ONLY a PROMPT.md file " +
|
|
944
|
-
"that a future autonomous session will execute. Do NOT create a JUDGE.md file.\n\n" +
|
|
945
|
-
"## Task\n" + (wData.task || "") +
|
|
946
|
-
"\n\n## Loop Directory\n" + lDir;
|
|
947
|
-
} else if (wData.judgeAuthor === "me") {
|
|
948
|
-
craftingPrompt = "Use the /clay-ralph skill to design a Ralph Loop for the following task. " +
|
|
949
|
-
"You MUST invoke the clay-ralph skill — do NOT execute the task yourself. " +
|
|
950
|
-
"The user will provide their own JUDGE.md, so create ONLY a PROMPT.md file " +
|
|
951
|
-
"that a future autonomous session will execute. Do NOT create a JUDGE.md file.\n\n" +
|
|
952
|
-
"## Task\n" + (wData.task || "") +
|
|
953
|
-
"\n\n## Loop Directory\n" + lDir;
|
|
954
|
-
} else {
|
|
955
|
-
craftingPrompt = "Use the /clay-ralph skill to design a Ralph Loop for the following task. " +
|
|
956
|
-
"You MUST invoke the clay-ralph skill — do NOT execute the task yourself. " +
|
|
957
|
-
"Your job is to interview me, then create PROMPT.md and JUDGE.md files " +
|
|
958
|
-
"that a future autonomous session will execute.\n\n" +
|
|
959
|
-
"## Task\n" + (wData.task || "") +
|
|
960
|
-
"\n\n## Loop Directory\n" + lDir;
|
|
961
|
-
}
|
|
995
|
+
var craftingPrompt = loopGuidance.legacyCraftPrompt({ simple: _draftIsSimple, userJudge: wData.judgeAuthor === "me", task: wData.task, directory: lDir });
|
|
962
996
|
|
|
963
997
|
// Pre-write user-provided files before crafting starts
|
|
964
998
|
if (wData.judgeText && wData.judgeAuthor === "me") {
|
|
@@ -1106,6 +1140,27 @@ function attachLoop(ctx) {
|
|
|
1106
1140
|
// --- Schedule create (from calendar click) ---
|
|
1107
1141
|
if (msg.type === "schedule_create") {
|
|
1108
1142
|
var sData = msg.data || {};
|
|
1143
|
+
var scheduleActor = ctx.getScheduleActor ? ctx.getScheduleActor(ws) : { ownerId: null, ownerName: null, sessionId: null };
|
|
1144
|
+
if (!scheduleActor) {
|
|
1145
|
+
sendTo(ws, { type: "loop_registry_error", text: "The schedule owner could not be verified." });
|
|
1146
|
+
return true;
|
|
1147
|
+
}
|
|
1148
|
+
if (sData.cron) {
|
|
1149
|
+
var scheduleCreateCron = validateCron(sData.cron);
|
|
1150
|
+
if (!scheduleCreateCron.ok) {
|
|
1151
|
+
sendTo(ws, { type: "loop_registry_error", text: scheduleCreateCron.error });
|
|
1152
|
+
return true;
|
|
1153
|
+
}
|
|
1154
|
+
sData.cron = scheduleCreateCron.cron;
|
|
1155
|
+
}
|
|
1156
|
+
if (sData.scheduleRule) {
|
|
1157
|
+
var scheduleCreateRule = validateScheduleRule(sData.scheduleRule);
|
|
1158
|
+
if (!scheduleCreateRule.ok) {
|
|
1159
|
+
sendTo(ws, { type: "loop_registry_error", text: scheduleCreateRule.error });
|
|
1160
|
+
return true;
|
|
1161
|
+
}
|
|
1162
|
+
sData.scheduleRule = scheduleCreateRule.value;
|
|
1163
|
+
}
|
|
1109
1164
|
loopRegistry.register({
|
|
1110
1165
|
name: sData.name || "Untitled",
|
|
1111
1166
|
task: sData.name || "",
|
|
@@ -1122,6 +1177,11 @@ function attachLoop(ctx) {
|
|
|
1122
1177
|
recurrenceEnd: sData.recurrenceEnd || null,
|
|
1123
1178
|
skipIfRunning: sData.skipIfRunning !== undefined ? sData.skipIfRunning : true,
|
|
1124
1179
|
intervalEnd: sData.intervalEnd || null,
|
|
1180
|
+
scheduleRule: sData.scheduleRule || null,
|
|
1181
|
+
ownerId: scheduleActor.ownerId,
|
|
1182
|
+
ownerName: scheduleActor.ownerName,
|
|
1183
|
+
sessionId: scheduleActor.sessionId,
|
|
1184
|
+
createdViaScheduledTasks: true,
|
|
1125
1185
|
});
|
|
1126
1186
|
return true;
|
|
1127
1187
|
}
|
|
@@ -1139,7 +1199,28 @@ function attachLoop(ctx) {
|
|
|
1139
1199
|
}
|
|
1140
1200
|
|
|
1141
1201
|
if (msg.type === "loop_registry_update") {
|
|
1142
|
-
|
|
1202
|
+
if (msg.data && msg.data.cron !== undefined && msg.data.cron !== null) {
|
|
1203
|
+
var scheduleUpdateCron = validateCron(msg.data.cron);
|
|
1204
|
+
if (!scheduleUpdateCron.ok) {
|
|
1205
|
+
sendTo(ws, { type: "loop_registry_error", text: scheduleUpdateCron.error });
|
|
1206
|
+
return true;
|
|
1207
|
+
}
|
|
1208
|
+
msg.data.cron = scheduleUpdateCron.cron;
|
|
1209
|
+
}
|
|
1210
|
+
if (msg.data && msg.data.scheduleRule) {
|
|
1211
|
+
var scheduleUpdateRule = validateScheduleRule(msg.data.scheduleRule);
|
|
1212
|
+
if (!scheduleUpdateRule.ok) {
|
|
1213
|
+
sendTo(ws, { type: "loop_registry_error", text: scheduleUpdateRule.error });
|
|
1214
|
+
return true;
|
|
1215
|
+
}
|
|
1216
|
+
msg.data.scheduleRule = scheduleUpdateRule.value;
|
|
1217
|
+
}
|
|
1218
|
+
var registryUpdateData = Object.assign({}, msg.data || {});
|
|
1219
|
+
delete registryUpdateData.ownerId;
|
|
1220
|
+
delete registryUpdateData.ownerName;
|
|
1221
|
+
delete registryUpdateData.sessionId;
|
|
1222
|
+
delete registryUpdateData.createdViaScheduledTasks;
|
|
1223
|
+
var updatedRec = loopRegistry.update(msg.id, registryUpdateData);
|
|
1143
1224
|
if (!updatedRec) {
|
|
1144
1225
|
sendTo(ws, { type: "loop_registry_error", text: "Record not found" });
|
|
1145
1226
|
}
|
|
@@ -1197,15 +1278,20 @@ function attachLoop(ctx) {
|
|
|
1197
1278
|
|
|
1198
1279
|
if (msg.type === "loop_registry_rerun") {
|
|
1199
1280
|
// Re-run an existing job (one-off from library)
|
|
1200
|
-
if (loopState.active || loopState.phase === "executing") {
|
|
1201
|
-
sendTo(ws, { type: "loop_registry_error", text: "A loop is already running" });
|
|
1202
|
-
return true;
|
|
1203
|
-
}
|
|
1204
1281
|
var rerunRec = loopRegistry.getById(msg.id);
|
|
1205
1282
|
if (!rerunRec) {
|
|
1206
1283
|
sendTo(ws, { type: "loop_registry_error", text: "Record not found" });
|
|
1207
1284
|
return true;
|
|
1208
1285
|
}
|
|
1286
|
+
if (usesPairExecution(rerunRec)) {
|
|
1287
|
+
var pairRun = ctx.executeScheduledTask && ctx.executeScheduledTask(rerunRec, "manual");
|
|
1288
|
+
if (!pairRun || !pairRun.ok) sendTo(ws, { type: "loop_registry_error", text: pairRun && pairRun.error || "The scheduled pair could not start." });
|
|
1289
|
+
return true;
|
|
1290
|
+
}
|
|
1291
|
+
if (loopState.active || loopState.phase === "executing") {
|
|
1292
|
+
sendTo(ws, { type: "loop_registry_error", text: "A loop is already running" });
|
|
1293
|
+
return true;
|
|
1294
|
+
}
|
|
1209
1295
|
var rerunDir = path.join(cwd, ".claude", "loops", rerunRec.id);
|
|
1210
1296
|
try {
|
|
1211
1297
|
fs.accessSync(path.join(rerunDir, "PROMPT.md"));
|
|
@@ -1297,7 +1383,7 @@ function attachLoop(ctx) {
|
|
|
1297
1383
|
getSchedules: function () { return loopRegistry.getAll(); },
|
|
1298
1384
|
importSchedule: function (data) { return loopRegistry.register(data); },
|
|
1299
1385
|
removeSchedule: function (id) { return loopRegistry.remove(id); },
|
|
1300
|
-
stopTimer: function () { loopRegistry.stopTimer(); },
|
|
1386
|
+
stopTimer: function () { if (durableSchedule) durableSchedule.shutdown(); else loopRegistry.stopTimer(); },
|
|
1301
1387
|
};
|
|
1302
1388
|
}
|
|
1303
1389
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
function stopAutonomousWork(session, roles, turnControl, taskControl, workerPermission) {
|
|
2
|
+
if (!roles) return false;
|
|
3
|
+
var runId = session.autonomousRun && session.autonomousRun.id;
|
|
4
|
+
var cancelled = taskControl.cancelAll(roles.worker, runId);
|
|
5
|
+
var owned = roles.worker._pairDelegation && roles.worker._pairDelegation.autonomousRunId === runId;
|
|
6
|
+
if (!owned && !cancelled) return true;
|
|
7
|
+
turnControl.markHumanStop(session);
|
|
8
|
+
if (!owned) return true;
|
|
9
|
+
taskControl.markInterruption(roles.worker, "user", "The Until complete run was stopped.");
|
|
10
|
+
workerPermission.cancelForSession(roles.worker, "The Until complete run was stopped.");
|
|
11
|
+
roles.worker.taskStopRequested = true;
|
|
12
|
+
if (roles.worker.abortController) roles.worker.abortController.abort();
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { stopAutonomousWork: stopAutonomousWork };
|