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,483 @@
|
|
|
1
|
+
var crypto = require("crypto");
|
|
2
|
+
var fs = require("fs");
|
|
3
|
+
var path = require("path");
|
|
4
|
+
var driverEligibility = require("./session-driver-eligibility");
|
|
5
|
+
var validation = require("./schedule-validation");
|
|
6
|
+
var sessionToolTransport = require("./yoke/session-tool-transport");
|
|
7
|
+
var createScheduledTaskRecords = require("./scheduled-task-records").createScheduledTaskRecords;
|
|
8
|
+
var SCHEDULE_RULE_SCHEMA = {
|
|
9
|
+
type: ["object", "null"],
|
|
10
|
+
description: "Server-local anchored cadence. Null when using advanced cron.",
|
|
11
|
+
properties: {
|
|
12
|
+
version: { type: "integer", enum: [1] }, timezone: { type: "string", enum: ["server-local"] },
|
|
13
|
+
anchorDate: { type: "string" }, time: { type: "string" },
|
|
14
|
+
recurrence: { type: ["object", "null"], properties: { every: { type: "integer", minimum: 1, maximum: 99 }, unit: { type: "string", enum: ["day", "week", "month", "year"] }, weekdays: { type: "array", items: { type: "integer", minimum: 0, maximum: 6 } } } },
|
|
15
|
+
recurrenceEnd: { type: ["object", "null"], properties: { type: { type: "string", enum: ["until", "after"] }, date: { type: "string" }, count: { type: "integer", minimum: 1, maximum: 999 } } },
|
|
16
|
+
interval: { type: ["object", "null"], properties: { every: { type: "integer", minimum: 1, maximum: 999 }, unit: { type: "string", enum: ["minute", "hour"] }, end: { type: ["object", "null"], properties: { type: { type: "string", enum: ["until", "after"] }, time: { type: "string" }, count: { type: "integer", minimum: 1, maximum: 999 } } } } },
|
|
17
|
+
}, required: ["version", "timezone", "anchorDate", "time"], additionalProperties: false,
|
|
18
|
+
};
|
|
19
|
+
var RUNTIME_SCHEMA = { type: "object", properties: { vendor: { type: "string" }, model: { type: ["string", "null"] }, effort: { type: ["string", "null"] } }, required: ["vendor"], additionalProperties: false };
|
|
20
|
+
var EXECUTION_SCHEMA = { type: ["object", "null"], properties: { driver: RUNTIME_SCHEMA, worker: RUNTIME_SCHEMA }, required: ["driver", "worker"], additionalProperties: false };
|
|
21
|
+
|
|
22
|
+
function clean(value, max) {
|
|
23
|
+
return typeof value === "string" ? value.trim().substring(0, max) : "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function activeInterviewId(session) {
|
|
27
|
+
var history = Array.isArray(session && session.history) ? session.history : [];
|
|
28
|
+
var current = null;
|
|
29
|
+
for (var i = 0; i < history.length; i++) {
|
|
30
|
+
var entry = history[i];
|
|
31
|
+
if (entry && (entry.source === "scheduled_task_interview" || entry.source === "scheduled_task_manual_start")) current = entry.interviewId || null;
|
|
32
|
+
if (entry && entry.source === "scheduled_task_interview_close" && entry.interviewId === current) current = null;
|
|
33
|
+
}
|
|
34
|
+
return current;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function interviewPrompt(vendor) {
|
|
38
|
+
var questionTool = sessionToolTransport.questionToolName(vendor || "claude");
|
|
39
|
+
var proposeTool = sessionToolTransport.scheduleToolName(vendor || "claude", "propose_scheduled_task");
|
|
40
|
+
return "You are helping the user create a scheduled task for this project. If the task description is blank, that is expected input for the interview, not a blocker: first ask what task they want scheduled. Use " + questionTool + " to ask exactly one concise question, wait for the answer, then ask the next question if needed. Never bundle questions. Establish a short name, precise instructions, the intended local date/time cadence, and independent Driver and Split Worker runtimes. Prefer scheduleRule for anchored day/week/month/year repeats and intra-day intervals; use cron only when the user explicitly needs an advanced custom expression. Explain the timing and execution pair in plain English and confirm them with the user. Then call " + proposeTool + " with the complete proposal. Do not create or run the task yourself; the human must use the final Create task control in the Scheduled Tasks window.";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function authorizeStoredOwner(record, ctx) {
|
|
44
|
+
var users = ctx.usersModule;
|
|
45
|
+
var multiUser = users.isMultiUser();
|
|
46
|
+
if (!record || !record.ownerId) return !multiUser && !ctx.osUsers;
|
|
47
|
+
var owner = users.findUserById(record.ownerId);
|
|
48
|
+
if (!owner) return false;
|
|
49
|
+
if (!users.getEffectivePermissions(owner, ctx.osUsers).scheduledTasks) return false;
|
|
50
|
+
if (!users.canAccessProject(record.ownerId, ctx.projectAccess)) return false;
|
|
51
|
+
if (!ctx.osUsers) return true;
|
|
52
|
+
if (!owner.linuxUser || !ctx.resolveLinuxUser(owner.linuxUser)) return false;
|
|
53
|
+
ctx.grantProjectAccess(owner.linuxUser);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function attachScheduledTasks(ctx) {
|
|
58
|
+
var sm = ctx.sm;
|
|
59
|
+
var registry = ctx.registry;
|
|
60
|
+
var records = createScheduledTaskRecords({ cwd: ctx.cwd, registry: registry, canUseSession: ctx.canUseSession, canAccessRecord: ctx.canAccessRecord, canClaimOwner: ctx.canClaimOwner, preflightRuntime: ctx.preflightRuntime, runtimeStatus: ctx.scheduledExecution ? function (record) { return ctx.scheduledExecution.runnable(record); } : null });
|
|
61
|
+
var schedulerTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || null;
|
|
62
|
+
|
|
63
|
+
function eligible(session) {
|
|
64
|
+
return !!(session && !ctx.isMate && (session.runtimeMode || session.mode) === "gui" && driverEligibility.isEligibleDriverSession(session) && !ctx.isDriverOperatedSession(session));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function available(session) {
|
|
68
|
+
if (!eligible(session)) return false;
|
|
69
|
+
if (!sm.sessions || sm.sessions.get(session.localId) !== session) return false;
|
|
70
|
+
return !ctx.canUseSession || ctx.canUseSession(session);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function toolsAvailable(session) {
|
|
74
|
+
return available(session) && sessionToolTransport.availableForSession(session).available;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function allowed(ws, session) {
|
|
78
|
+
if (!eligible(session)) return false;
|
|
79
|
+
if (!sm.sessions || sm.sessions.get(session.localId) !== session) return false;
|
|
80
|
+
if (ctx.canAccess && !ctx.canAccess(ws, session)) return false;
|
|
81
|
+
return !ctx.hasPermission || ctx.hasPermission(ws);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function record(session, entry) {
|
|
85
|
+
session.history.push(entry);
|
|
86
|
+
sm.appendToSessionFile(session, entry);
|
|
87
|
+
sm.saveSessionFile(session);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function publish(session, options) {
|
|
91
|
+
sm.sendToSession(session, {
|
|
92
|
+
type: "scheduled_task_interview_state",
|
|
93
|
+
sessionId: session.localId,
|
|
94
|
+
interviewId: activeInterviewId(session),
|
|
95
|
+
draft: session.scheduledTaskDraft || null,
|
|
96
|
+
timeZone: schedulerTimeZone,
|
|
97
|
+
open: !!(options && options.open),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function begin(session, requestId) {
|
|
102
|
+
var existing = activeInterviewId(session);
|
|
103
|
+
if (existing) {
|
|
104
|
+
publish(session, { open: true });
|
|
105
|
+
return { ok: true, duplicate: true, interviewId: existing };
|
|
106
|
+
}
|
|
107
|
+
var history = session.history || [];
|
|
108
|
+
for (var i = history.length - 1; i >= 0; i--) {
|
|
109
|
+
if (history[i] && history[i].source === "scheduled_task_interview" && history[i].requestId === requestId) {
|
|
110
|
+
return { ok: true, duplicate: true, closed: true, interviewId: history[i].interviewId };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
var interviewId = crypto.randomUUID();
|
|
114
|
+
session.scheduledTaskDraft = null;
|
|
115
|
+
record(session, { type: "user_message", text: "[Scheduled task interview started]", source: "scheduled_task_interview", interviewId: interviewId, requestId: requestId, _ts: Date.now(), _internal: true });
|
|
116
|
+
publish(session, { open: true });
|
|
117
|
+
return { ok: true, interviewId: interviewId };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function beginManual(session, requestId) {
|
|
121
|
+
var existing = activeInterviewId(session);
|
|
122
|
+
if (session.scheduledTaskDraft) {
|
|
123
|
+
if (!existing || session.scheduledTaskDraft.interviewId !== existing) return { ok: false, error: "The existing schedule draft could not be safely resumed." };
|
|
124
|
+
publish(session, { open: true });
|
|
125
|
+
return { ok: true, duplicate: true, resumed: true, interviewId: existing };
|
|
126
|
+
}
|
|
127
|
+
if (existing) {
|
|
128
|
+
return { ok: false, error: "Finish or cancel the active schedule interview before starting a manual draft." };
|
|
129
|
+
}
|
|
130
|
+
var history = session.history || [];
|
|
131
|
+
for (var i = history.length - 1; i >= 0; i--) {
|
|
132
|
+
if (history[i] && history[i].source === "scheduled_task_manual_start" && history[i].requestId === requestId) {
|
|
133
|
+
return { ok: true, duplicate: true, closed: true, interviewId: history[i].interviewId };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
var interviewId = crypto.randomUUID();
|
|
137
|
+
session.scheduledTaskDraft = {
|
|
138
|
+
id: crypto.randomUUID(), version: 1, interviewId: interviewId, sessionId: session.localId,
|
|
139
|
+
manual: true, name: "", instructions: "", cron: "", execution: null, skipIfRunning: true, createdAt: Date.now(),
|
|
140
|
+
};
|
|
141
|
+
record(session, { type: "scheduled_task_manual_start", source: "scheduled_task_manual_start", interviewId: interviewId, requestId: requestId, _ts: Date.now(), _internal: true });
|
|
142
|
+
publish(session, { open: true });
|
|
143
|
+
return { ok: true, interviewId: interviewId };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function toolResult(value) {
|
|
147
|
+
return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function propose(session, capturedInterviewId, queryGeneration, args) {
|
|
151
|
+
if (!available(session) || Number(session._sdkQueryGeneration || 0) !== queryGeneration || !capturedInterviewId || activeInterviewId(session) !== capturedInterviewId) {
|
|
152
|
+
return toolResult({ status: "rejected", reason: "This scheduled-task proposal belongs to a stale or unavailable interview." });
|
|
153
|
+
}
|
|
154
|
+
var checked = validation.validateScheduledTaskInput(args);
|
|
155
|
+
if (!checked.ok) return toolResult({ status: "rejected", reason: checked.error });
|
|
156
|
+
var prior = session.scheduledTaskDraft;
|
|
157
|
+
var draft = Object.assign({}, checked.value, {
|
|
158
|
+
id: prior && prior.id || crypto.randomUUID(),
|
|
159
|
+
version: prior && prior.interviewId === capturedInterviewId ? Number(prior.version) + 1 : 1,
|
|
160
|
+
interviewId: capturedInterviewId,
|
|
161
|
+
sessionId: session.localId,
|
|
162
|
+
createdAt: prior && prior.createdAt || Date.now(),
|
|
163
|
+
});
|
|
164
|
+
session.scheduledTaskDraft = draft;
|
|
165
|
+
sm.saveSessionFile(session);
|
|
166
|
+
publish(session, { open: true });
|
|
167
|
+
return toolResult({ status: "proposed", draft: draft });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getToolDefs(session) {
|
|
171
|
+
if (!toolsAvailable(session)) return [];
|
|
172
|
+
var toolInterviewId = activeInterviewId(session);
|
|
173
|
+
var queryGeneration = Number(session._sdkQueryGeneration || 0);
|
|
174
|
+
return [
|
|
175
|
+
{
|
|
176
|
+
name: "begin_scheduled_task_interview",
|
|
177
|
+
queryBound: true,
|
|
178
|
+
scheduleInterviewActive: function () { return !!activeInterviewId(session); },
|
|
179
|
+
structuredQuestionLimit: function () { return available(session) && Number(session._sdkQueryGeneration || 0) === queryGeneration && activeInterviewId(session) ? 1 : null; },
|
|
180
|
+
description: "Open this project's Scheduled Tasks workbench and begin a schedule interview in this same Driver query. Use this when the user asks in natural language to create or schedule recurring work. Missing task details are expected interview input: ask the user for them with structured questions. Never create a new session from this tool.",
|
|
181
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
182
|
+
handler: function () {
|
|
183
|
+
if (!available(session) || Number(session._sdkQueryGeneration || 0) !== queryGeneration) return toolResult({ status: "rejected", reason: "This scheduled-task tool belongs to an older query." });
|
|
184
|
+
session._scheduledTaskStartToken = crypto.randomUUID();
|
|
185
|
+
var result = begin(session, "tool-" + crypto.randomUUID());
|
|
186
|
+
if (result.ok && !result.duplicate) toolInterviewId = result.interviewId;
|
|
187
|
+
return toolResult(Object.assign({}, result, { status: result.ok ? "interview_started" : "rejected", guidance: interviewPrompt(session.vendor || "claude") }));
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: "propose_scheduled_task",
|
|
192
|
+
queryBound: true,
|
|
193
|
+
description: "Publish the reviewed scheduled-task draft into the open workbench. Prefer an anchored scheduleRule for normal recurrence and interval controls; use cron only for an explicit advanced custom expression. This does not create the task; the human explicitly creates it there.",
|
|
194
|
+
inputSchema: { type: "object", properties: {
|
|
195
|
+
name: { type: "string" }, instructions: { type: "string" }, cron: { type: ["string", "null"] }, date: { type: "string" }, time: { type: "string" },
|
|
196
|
+
scheduleRule: SCHEDULE_RULE_SCHEMA,
|
|
197
|
+
execution: EXECUTION_SCHEMA, skipIfRunning: { type: "boolean" },
|
|
198
|
+
}, required: ["name", "instructions", "cron"], additionalProperties: false },
|
|
199
|
+
handler: function (args) {
|
|
200
|
+
return propose(session, toolInterviewId, queryGeneration, args || {});
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: "list_scheduled_tasks", queryBound: true,
|
|
205
|
+
description: "List scheduled tasks owned by this Driver session. Use this to identify an existing task before reading or editing it.",
|
|
206
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
207
|
+
handler: function () {
|
|
208
|
+
if (!available(session) || Number(session._sdkQueryGeneration || 0) !== queryGeneration) return toolResult({ status: "rejected", reason: "This scheduled-task tool belongs to an older query." });
|
|
209
|
+
return toolResult({ status: "ok", tasks: records.list(session) });
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: "read_scheduled_task", queryBound: true,
|
|
214
|
+
description: "Read one scheduled task by stable id, including its revision, owner, schedule, instructions, and runtime settings.",
|
|
215
|
+
inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"], additionalProperties: false },
|
|
216
|
+
handler: function (args) {
|
|
217
|
+
if (!available(session) || Number(session._sdkQueryGeneration || 0) !== queryGeneration) return toolResult({ status: "rejected", reason: "This scheduled-task tool belongs to an older query." });
|
|
218
|
+
var result = records.read(session, clean(args && args.id, 200));
|
|
219
|
+
if (result.ok) publishRecord(session, result.record);
|
|
220
|
+
return toolResult(result);
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: "update_scheduled_task", queryBound: true,
|
|
225
|
+
description: "Update the exact existing scheduled task by stable id and revision. Omitted fields are preserved; this never creates a duplicate task.",
|
|
226
|
+
inputSchema: { type: "object", properties: {
|
|
227
|
+
id: { type: "string" }, revision: { type: "number" }, name: { type: "string" }, instructions: { type: "string" }, cron: { type: ["string", "null"] },
|
|
228
|
+
date: { type: "string" }, time: { type: "string" }, scheduleRule: SCHEDULE_RULE_SCHEMA, execution: EXECUTION_SCHEMA, skipIfRunning: { type: "boolean" }, claimOwner: { type: "boolean" },
|
|
229
|
+
}, required: ["id", "revision"], additionalProperties: false },
|
|
230
|
+
handler: function (args) {
|
|
231
|
+
if (!available(session) || Number(session._sdkQueryGeneration || 0) !== queryGeneration) return toolResult({ status: "rejected", reason: "This scheduled-task tool belongs to an older query." });
|
|
232
|
+
var patch = Object.assign({}, args || {}); delete patch.id; delete patch.revision;
|
|
233
|
+
var result = records.update(session, clean(args && args.id, 200), args && args.revision, patch);
|
|
234
|
+
if (result.ok) publishRecord(session, result.record);
|
|
235
|
+
return toolResult(result);
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function publishRecord(session, record) {
|
|
242
|
+
sm.sendToSession(session, { type: "scheduled_task_driver_edit_state", sessionId: session.localId, record: record, open: true });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function owner(ws) {
|
|
246
|
+
var user = ws && ws._clayUser;
|
|
247
|
+
return { id: user && user.id || null, name: user && (user.displayName || user.username) || null };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function list(ws, requestId, session) {
|
|
251
|
+
var taskRecords = registry.getAll();
|
|
252
|
+
var items = [];
|
|
253
|
+
for (var i = 0; i < taskRecords.length; i++) {
|
|
254
|
+
var rec = taskRecords[i];
|
|
255
|
+
if (!rec.cron && !rec.scheduleRule && !(rec.source === "schedule" && rec.date && rec.time)) continue;
|
|
256
|
+
if (!records.canAccess(session, rec)) continue;
|
|
257
|
+
items.push(Object.assign({}, rec, {
|
|
258
|
+
ownerName: rec.ownerName || (rec.ownerId && ctx.resolveOwnerName && ctx.resolveOwnerName(rec.ownerId)) || "Unassigned",
|
|
259
|
+
projectSlug: ctx.projectSlug || null,
|
|
260
|
+
projectTitle: ctx.projectTitle || ctx.projectSlug || "Project",
|
|
261
|
+
readiness: ctx.scheduledExecution ? ctx.scheduledExecution.runnable(rec) : null,
|
|
262
|
+
latestRun: Array.isArray(rec.runs) && rec.runs.length ? rec.runs[rec.runs.length - 1] : null,
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
ctx.sendTo(ws, { type: "scheduled_tasks_state", requestId: requestId || null, records: items, timeZone: schedulerTimeZone });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function engineState(ws, msg, session) {
|
|
269
|
+
var userId = ws && ws._clayUser ? ws._clayUser.id : "default";
|
|
270
|
+
var preference = ctx.usersModule && ctx.usersModule.getScheduledTaskInterviewEngine ? ctx.usersModule.getScheduledTaskInterviewEngine(userId) : {};
|
|
271
|
+
var installed = (sm.installedVendors || []).filter(function (vendor) { return sessionToolTransport.capability(vendor).kind !== "none"; });
|
|
272
|
+
var runtimeInstalled = sm.installedVendors || [];
|
|
273
|
+
var requestedVendor = clean(msg && msg.vendor, 40);
|
|
274
|
+
var targetVendor = runtimeInstalled.indexOf(requestedVendor) !== -1 ? requestedVendor : (installed.indexOf(preference.vendor) !== -1 ? preference.vendor : installed[0] || "");
|
|
275
|
+
var catalogError = "";
|
|
276
|
+
if (targetVendor && ctx.getEngineCatalog) {
|
|
277
|
+
try {
|
|
278
|
+
var catalog = await ctx.getEngineCatalog(ws, targetVendor);
|
|
279
|
+
if (!catalog || catalog.status !== "ready") catalogError = catalog && catalog.error || "Models are not available for this interview engine yet.";
|
|
280
|
+
} catch (error) { catalogError = error.message || String(error); }
|
|
281
|
+
}
|
|
282
|
+
var models = {};
|
|
283
|
+
var ready = {};
|
|
284
|
+
for (var i = 0; i < runtimeInstalled.length; i++) {
|
|
285
|
+
models[runtimeInstalled[i]] = (sm.modelsByVendor && sm.modelsByVendor[runtimeInstalled[i]] || []).slice();
|
|
286
|
+
ready[runtimeInstalled[i]] = models[runtimeInstalled[i]].length > 0;
|
|
287
|
+
}
|
|
288
|
+
ctx.sendTo(ws, { type: "scheduled_task_interview_engine_state", requestId: msg.requestId || null, projectSlug: ctx.projectSlug || null, sessionId: session.localId, installedVendors: installed, runtimeInstalledVendors: runtimeInstalled.slice(), modelsByVendor: models, catalogReadyByVendor: ready, catalogVendor: targetVendor, error: catalogError, preference: preference || {} });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function createTaskFiles(id, task) {
|
|
292
|
+
if (typeof id !== "string" || !/^[A-Za-z0-9_-]{1,200}$/.test(id)) throw new Error("Task storage identity is invalid.");
|
|
293
|
+
var dir = path.join(ctx.cwd, ".claude", "loops", id);
|
|
294
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
295
|
+
fs.writeFileSync(path.join(dir, "PROMPT.md"), task.instructions + "\n", "utf8");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function createFromDraft(ws, session, msg) {
|
|
299
|
+
var requestId = clean(msg.requestId, 200);
|
|
300
|
+
var history = session.history || [];
|
|
301
|
+
for (var i = 0; i < history.length; i++) {
|
|
302
|
+
if (history[i] && history[i].source === "scheduled_task_created" && history[i].requestId === requestId) return { ok: true, duplicate: true, recordId: history[i].recordId };
|
|
303
|
+
}
|
|
304
|
+
var draft = session.scheduledTaskDraft;
|
|
305
|
+
if (!draft || draft.id !== clean(msg.proposalId, 200) || Number(draft.version) !== Number(msg.version) || draft.interviewId !== activeInterviewId(session)) {
|
|
306
|
+
return { ok: false, error: "This scheduled-task draft is stale or no longer pending." };
|
|
307
|
+
}
|
|
308
|
+
var taskInput = msg.data || draft;
|
|
309
|
+
var checked = validation.validateScheduledTaskInput(taskInput);
|
|
310
|
+
if (!checked.ok) return { ok: false, error: checked.error };
|
|
311
|
+
if (!checked.value.execution) return { ok: false, error: "Choose both a Driver and a Split Worker runtime." };
|
|
312
|
+
if (ctx.preflightRuntime) {
|
|
313
|
+
try {
|
|
314
|
+
ctx.preflightRuntime(checked.value.execution.driver, null, true);
|
|
315
|
+
ctx.preflightRuntime(checked.value.execution.worker, null, true);
|
|
316
|
+
} catch (runtimeError) { return { ok: false, error: runtimeError.message || String(runtimeError) }; }
|
|
317
|
+
}
|
|
318
|
+
var recordId = "loop_" + Date.now() + "_" + crypto.randomBytes(3).toString("hex");
|
|
319
|
+
var saved;
|
|
320
|
+
try {
|
|
321
|
+
var actor = owner(ws);
|
|
322
|
+
if (session.ownerId && actor.id !== session.ownerId) return { ok: false, error: "The schedule owner does not match this Driver session." };
|
|
323
|
+
createTaskFiles(recordId, checked.value);
|
|
324
|
+
saved = registry.register(Object.assign({}, checked.value, {
|
|
325
|
+
id: recordId, task: checked.value.instructions, prompt: checked.value.instructions, enabled: true,
|
|
326
|
+
mode: "simple", source: checked.value.cron ? null : "schedule", ownerId: actor.id, ownerName: actor.name, sessionId: session.localId, createdViaScheduledTasks: true,
|
|
327
|
+
}));
|
|
328
|
+
if (!saved || (registry.getLastSaveError && registry.getLastSaveError())) throw new Error(registry.getLastSaveError() || "The schedule registry did not save the task.");
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (saved && saved.id && registry.remove) {
|
|
331
|
+
try { registry.remove(saved.id); } catch (removeError) {}
|
|
332
|
+
}
|
|
333
|
+
try { fs.rmSync(path.join(ctx.cwd, ".claude", "loops", recordId), { recursive: true, force: true }); } catch (removeFileError) {}
|
|
334
|
+
return { ok: false, error: "The task could not be stored: " + (error.message || error) };
|
|
335
|
+
}
|
|
336
|
+
record(session, { type: "scheduled_task_created", source: "scheduled_task_created", interviewId: draft.interviewId, proposalId: draft.id, proposalVersion: draft.version, requestId: requestId, recordId: saved.id, _ts: Date.now() });
|
|
337
|
+
record(session, { type: "scheduled_task_interview_close", source: "scheduled_task_interview_close", interviewId: draft.interviewId, _ts: Date.now() });
|
|
338
|
+
session.scheduledTaskDraft = null;
|
|
339
|
+
sm.saveSessionFile(session);
|
|
340
|
+
publish(session);
|
|
341
|
+
return { ok: true, recordId: saved.id };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function handleMessage(ws, msg) {
|
|
345
|
+
if (["scheduled_tasks_list", "scheduled_task_interview_engine_get", "scheduled_task_interview_state", "scheduled_task_interview_start", "scheduled_task_manual_start", "scheduled_task_create", "scheduled_task_update", "scheduled_task_cancel", "scheduled_task_run", "scheduled_task_toggle", "scheduled_task_delete", "scheduled_task_open_run"].indexOf(msg.type) === -1) return false;
|
|
346
|
+
var session = ctx.getSessionForWs(ws);
|
|
347
|
+
if (!session || !allowed(ws, session) || (msg.sessionId !== undefined && Number(msg.sessionId) !== Number(session.localId))) {
|
|
348
|
+
ctx.sendTo(ws, { type: msg.type === "scheduled_task_manual_start" ? "scheduled_task_manual_start_result" : "scheduled_task_action_result", sessionId: session && session.localId || null, requestId: msg.requestId || null, ok: false, error: "Scheduled Tasks is unavailable in this session." });
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
if (msg.type === "scheduled_tasks_list") { list(ws, msg.requestId, session); return true; }
|
|
352
|
+
if (msg.type === "scheduled_task_run" || msg.type === "scheduled_task_toggle" || msg.type === "scheduled_task_delete" || msg.type === "scheduled_task_open_run") {
|
|
353
|
+
var actionRequestId = clean(msg.requestId, 200);
|
|
354
|
+
var actionFingerprint = msg.type + ":" + clean(msg.id, 200) + ":" + Number(msg.version);
|
|
355
|
+
var priorAction = null;
|
|
356
|
+
for (var hi = (session.history || []).length - 1; hi >= 0; hi--) {
|
|
357
|
+
if (session.history[hi] && session.history[hi].source === "scheduled_task_action" && session.history[hi].requestId === actionRequestId) { priorAction = session.history[hi]; break; }
|
|
358
|
+
}
|
|
359
|
+
if (!actionRequestId || (priorAction && priorAction.fingerprint !== actionFingerprint)) {
|
|
360
|
+
ctx.sendTo(ws, { type: "scheduled_task_action_result", requestId: msg.requestId || null, action: msg.type, ok: false, error: "A unique correlated scheduled-task action is required." });
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
if (priorAction) { ctx.sendTo(ws, Object.assign({}, priorAction.result, { duplicate: true })); return true; }
|
|
364
|
+
var actionRecord = registry.getById(clean(msg.id, 200));
|
|
365
|
+
if (!actionRecord || !records.canAccess(session, actionRecord) || Number(msg.version) !== Number(actionRecord.updatedAt)) {
|
|
366
|
+
ctx.sendTo(ws, { type: "scheduled_task_action_result", requestId: msg.requestId || null, action: msg.type, ok: false, error: "This scheduled task changed or is unavailable." });
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
var actionOk = false; var actionError = null; var openSessionId = null;
|
|
370
|
+
if (msg.type === "scheduled_task_open_run") {
|
|
371
|
+
var runRef = actionRecord.activeRun || (Array.isArray(actionRecord.runs) && actionRecord.runs.length ? actionRecord.runs[actionRecord.runs.length - 1] : null);
|
|
372
|
+
if (runRef && runRef.driverOriginId) sm.sessions.forEach(function (candidate) {
|
|
373
|
+
var marker = candidate.scheduledTaskRun;
|
|
374
|
+
if (openSessionId === null && candidate.ownerId === actionRecord.ownerId && candidate.sessionOriginId === runRef.driverOriginId && marker && marker.role === "driver" && marker.scheduleId === actionRecord.id && marker.runId === runRef.runId) openSessionId = candidate.localId;
|
|
375
|
+
});
|
|
376
|
+
actionOk = openSessionId !== null; actionError = actionOk ? null : "The saved Driver session for this run is unavailable.";
|
|
377
|
+
} else if (msg.type === "scheduled_task_run") {
|
|
378
|
+
var runResult = ctx.scheduledExecution.trigger(actionRecord, "manual"); actionOk = !!runResult.ok; actionError = runResult.error || null;
|
|
379
|
+
} else if (msg.type === "scheduled_task_toggle") actionOk = !!registry.toggleEnabled(actionRecord.id);
|
|
380
|
+
else {
|
|
381
|
+
if (actionRecord.activeRun) ctx.scheduledExecution.stopRecord(actionRecord, "The scheduled task was deleted.");
|
|
382
|
+
actionOk = registry.remove(actionRecord.id);
|
|
383
|
+
}
|
|
384
|
+
var actionResult = { type: "scheduled_task_action_result", requestId: msg.requestId || null, action: msg.type, ok: actionOk, sessionId: openSessionId, error: actionOk ? null : actionError || "The scheduled task action failed." };
|
|
385
|
+
record(session, { type: "scheduled_task_action", source: "scheduled_task_action", requestId: actionRequestId, fingerprint: actionFingerprint, result: actionResult, _ts: Date.now() });
|
|
386
|
+
ctx.sendTo(ws, actionResult);
|
|
387
|
+
if (actionOk) list(ws, null, session);
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
if (msg.type === "scheduled_task_interview_engine_get") {
|
|
391
|
+
Promise.resolve(engineState(ws, msg, session)).catch(function (error) {
|
|
392
|
+
ctx.sendTo(ws, { type: "scheduled_task_interview_engine_state", requestId: msg.requestId || null, projectSlug: ctx.projectSlug || null, sessionId: session.localId, error: error.message || String(error), installedVendors: [], modelsByVendor: {}, catalogReadyByVendor: {}, preference: {} });
|
|
393
|
+
});
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
if (msg.type === "scheduled_task_interview_state") { publish(session); return true; }
|
|
397
|
+
if (msg.type === "scheduled_task_manual_start") {
|
|
398
|
+
var manualRequestId = clean(msg.requestId, 200);
|
|
399
|
+
var manual = manualRequestId ? beginManual(session, manualRequestId) : { ok: false, error: "A correlated manual draft request is required." };
|
|
400
|
+
ctx.sendTo(ws, Object.assign({ type: "scheduled_task_manual_start_result", sessionId: session.localId, requestId: msg.requestId || null }, manual));
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
if (msg.type === "scheduled_task_interview_start") {
|
|
404
|
+
var toolAvailability = sessionToolTransport.availableForSession(session);
|
|
405
|
+
if (!toolAvailability.available) {
|
|
406
|
+
ctx.sendTo(ws, { type: "scheduled_task_interview_start_result", sessionId: session.localId, requestId: msg.requestId || null, ok: false, error: toolAvailability.reason });
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
if (session.isProcessing || session._queryStarting) {
|
|
410
|
+
ctx.sendTo(ws, { type: "scheduled_task_interview_start_result", sessionId: session.localId, requestId: msg.requestId || null, ok: false, error: "Wait for the current task to finish." });
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
var started = begin(session, clean(msg.requestId, 200));
|
|
414
|
+
if (started.duplicate) {
|
|
415
|
+
ctx.sendTo(ws, { type: "scheduled_task_interview_start_result", sessionId: session.localId, requestId: msg.requestId || null, ok: true, duplicate: true, interviewId: started.interviewId });
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
session.isProcessing = true;
|
|
419
|
+
var startToken = crypto.randomUUID();
|
|
420
|
+
session._scheduledTaskStartToken = startToken;
|
|
421
|
+
ctx.onProcessingChanged();
|
|
422
|
+
ctx.sendToSession(session.localId, { type: "status", status: "processing" });
|
|
423
|
+
try {
|
|
424
|
+
var query = ctx.sdk.startQuery(session, interviewPrompt(session.vendor || "claude"), null, ctx.ensureProjectAccessForSession(session));
|
|
425
|
+
var startQueryGeneration = Number(session._sdkQueryGeneration || 0);
|
|
426
|
+
ctx.sendTo(ws, { type: "scheduled_task_interview_start_result", sessionId: session.localId, requestId: msg.requestId || null, ok: true, interviewId: started.interviewId });
|
|
427
|
+
Promise.resolve(query).catch(function (error) {
|
|
428
|
+
if (activeInterviewId(session) !== started.interviewId || session._scheduledTaskStartToken !== startToken || Number(session._sdkQueryGeneration || 0) !== startQueryGeneration) return;
|
|
429
|
+
session.isProcessing = false;
|
|
430
|
+
ctx.onProcessingChanged();
|
|
431
|
+
ctx.sendToSession(session.localId, { type: "status", status: "idle" });
|
|
432
|
+
if (!session.scheduledTaskDraft) record(session, { type: "scheduled_task_interview_close", source: "scheduled_task_interview_close", interviewId: started.interviewId, _ts: Date.now() });
|
|
433
|
+
ctx.sendTo(ws, { type: "scheduled_task_interview_start_result", sessionId: session.localId, requestId: msg.requestId || null, ok: false, error: error.message || String(error) });
|
|
434
|
+
});
|
|
435
|
+
} catch (error) {
|
|
436
|
+
session.isProcessing = false;
|
|
437
|
+
ctx.onProcessingChanged();
|
|
438
|
+
if (!session.scheduledTaskDraft) record(session, { type: "scheduled_task_interview_close", source: "scheduled_task_interview_close", interviewId: started.interviewId, _ts: Date.now() });
|
|
439
|
+
ctx.sendTo(ws, { type: "scheduled_task_interview_start_result", sessionId: session.localId, requestId: msg.requestId || null, ok: false, error: error.message || String(error) });
|
|
440
|
+
}
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
if (msg.type === "scheduled_task_create") {
|
|
444
|
+
var result = createFromDraft(ws, session, msg);
|
|
445
|
+
ctx.sendTo(ws, Object.assign({ type: "scheduled_task_action_result", requestId: msg.requestId || null, action: "create" }, result));
|
|
446
|
+
if (result.ok) list(ws, null, session);
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
if (msg.type === "scheduled_task_cancel") {
|
|
450
|
+
var draft = session.scheduledTaskDraft;
|
|
451
|
+
var cancelInterviewId = draft && draft.id === msg.proposalId && Number(draft.version) === Number(msg.version) ? draft.interviewId : clean(msg.interviewId, 200);
|
|
452
|
+
if (cancelInterviewId && cancelInterviewId === activeInterviewId(session)) {
|
|
453
|
+
session._scheduledTaskStartToken = crypto.randomUUID();
|
|
454
|
+
record(session, { type: "scheduled_task_interview_close", source: "scheduled_task_interview_close", interviewId: cancelInterviewId, _ts: Date.now() });
|
|
455
|
+
session.scheduledTaskDraft = null; sm.saveSessionFile(session); publish(session);
|
|
456
|
+
}
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
var updated = records.update(session, clean(msg.id, 200), msg.version, msg.data || {});
|
|
460
|
+
ctx.sendTo(ws, { type: "scheduled_task_action_result", requestId: msg.requestId || null, action: "update", ok: updated.ok, recordId: updated.record && updated.record.id || null, error: updated.error || null });
|
|
461
|
+
if (!updated.ok) return true;
|
|
462
|
+
list(ws, null, session);
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function getSystemPrompt(session) {
|
|
467
|
+
if (!toolsAvailable(session)) return "";
|
|
468
|
+
var vendor = session.vendor || "claude";
|
|
469
|
+
return "When the user asks in natural language to create recurring or scheduled project work, call " + sessionToolTransport.scheduleToolName(vendor, "begin_scheduled_task_interview") + ". Continue in this same query, asking exactly one question at a time with " + sessionToolTransport.questionToolName(vendor) + ", then call " + sessionToolTransport.scheduleToolName(vendor, "propose_scheduled_task") + ". Missing task details are expected interview input, not a blocker. When the user asks to inspect or edit an existing scheduled task, use " + sessionToolTransport.scheduleToolName(vendor, "list_scheduled_tasks") + ", " + sessionToolTransport.scheduleToolName(vendor, "read_scheduled_task") + ", and " + sessionToolTransport.scheduleToolName(vendor, "update_scheduled_task") + "; update by stable id and revision and never create a duplicate. Never create a sibling session for an existing-conversation request.";
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function canStartInterview(ws, session) {
|
|
473
|
+
return allowed(ws, session) && toolsAvailable(session);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function canCreateInterview(ws, session) {
|
|
477
|
+
return allowed(ws, session) && available(session);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return { getToolDefs: getToolDefs, getSystemPrompt: getSystemPrompt, handleMessage: handleMessage, activeInterviewId: activeInterviewId, canStartInterview: canStartInterview, canCreateInterview: canCreateInterview };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
module.exports = { attachScheduledTasks: attachScheduledTasks, activeInterviewId: activeInterviewId, interviewPrompt: interviewPrompt, authorizeStoredOwner: authorizeStoredOwner };
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
var pairMcp = require("./session-pair-mcp-server"), pairPrompts = require("./session-pair-prompts");
|
|
2
|
-
var { attachPairFactory } = require("./session-pair-factory"), { attachPairLifecycle } = require("./project-pair-lifecycle"),
|
|
1
|
+
var pairMcp = require("./session-pair-mcp-server"), pairPrompts = require("./session-pair-prompts"); var { attachPairFactory } = require("./session-pair-factory"), { attachPairLifecycle } = require("./project-pair-lifecycle"),
|
|
3
2
|
{ attachPairTurnControl } = require("./session-pair-turn-control");
|
|
4
|
-
var driverEligibility = require("./session-driver-eligibility"), driverOrchestration = require("./session-driver-orchestration");
|
|
5
|
-
var {
|
|
6
|
-
var { attachPairMessage } = require("./project-pair-message"), { attachPairTaskControl } = require("./project-pair-task-control");
|
|
3
|
+
var driverEligibility = require("./session-driver-eligibility"), driverOrchestration = require("./session-driver-orchestration"), { attachWorkerPermission } = require("./project-worker-permission"), { attachWorkerProposal } = require("./project-worker-proposal");
|
|
4
|
+
var { attachPairMessage } = require("./project-pair-message"), { attachPairTaskControl } = require("./project-pair-task-control"), stopAutonomous = require("./project-pair-autonomous-stop").stopAutonomousWork, stopOwned = require("./project-pair-owned-stop").stopOwnedPair;
|
|
7
5
|
var pairHistory = require("./session-pair-history"), responseText = pairHistory.responseText, errorSince = pairHistory.errorSince, recentTurns = pairHistory.recentTurns, operationFingerprint = require("./session-pair-operation").fingerprint;
|
|
6
|
+
var beginPairQuery = require("./session-pair-query-start").beginPairQuery;
|
|
8
7
|
function toolResult(value) { return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] }); }
|
|
9
|
-
function toolError(err) {
|
|
10
|
-
return Promise.resolve({ content: [{ type: "text", text: "Error: " + (err.message || err) }], isError: true });
|
|
11
|
-
}
|
|
8
|
+
function toolError(err) { return Promise.resolve({ content: [{ type: "text", text: "Error: " + (err.message || err) }], isError: true }); }
|
|
12
9
|
function attachSessionPair(ctx) {
|
|
13
10
|
var sm = ctx.sm;
|
|
14
11
|
var store = ctx.splitStore;
|
|
@@ -95,12 +92,7 @@ function attachSessionPair(ctx) {
|
|
|
95
92
|
sm.sendToSession(caller, { type: "status", status: "processing" });
|
|
96
93
|
}
|
|
97
94
|
if (!sdk.pushMessage(caller, text)) {
|
|
98
|
-
|
|
99
|
-
Promise.resolve(sdk.startQuery(caller, text, undefined, ctx.getLinuxUserForSession(caller))).catch(function (err) {
|
|
100
|
-
caller.isProcessing = false;
|
|
101
|
-
sm.sendAndRecord(caller, { type: "error", text: err.message || String(err) });
|
|
102
|
-
ctx.onProcessingChanged();
|
|
103
|
-
});
|
|
95
|
+
beginPairQuery({ sdk: sdk, sm: sm, linuxUser: ctx.getLinuxUserForSession(caller), onProcessingChanged: ctx.onProcessingChanged }, caller, text);
|
|
104
96
|
}
|
|
105
97
|
sm.broadcastSessionList();
|
|
106
98
|
return true;
|
|
@@ -147,6 +139,7 @@ function attachSessionPair(ctx) {
|
|
|
147
139
|
if (token.interrupted) taskControl.ensureSystemInterruption(partner, token);
|
|
148
140
|
taskControl.complete(partner, caller, token, token.failure ? "failed" : (token.interrupted ? "interrupted" : "completed"), token.response, token.failure);
|
|
149
141
|
finishDelegation(group, caller, partner, token);
|
|
142
|
+
if (typeof ctx.onPartnerResult === "function") ctx.onPartnerResult(caller, partner, token);
|
|
150
143
|
var resumed = resumeDriverWithResult(caller, partner, token);
|
|
151
144
|
taskControl.drain(caller, partner);
|
|
152
145
|
return resumed;
|
|
@@ -345,12 +338,15 @@ function attachSessionPair(ctx) {
|
|
|
345
338
|
workerPermission.cancelForSession(roles.worker, "The human stopped this Split Worker turn.");
|
|
346
339
|
return true;
|
|
347
340
|
}
|
|
341
|
+
function stopOwnedPair(session, reason) { return stopOwned(session, rolesFor(session), turnControl, taskControl, workerPermission, reason); }
|
|
342
|
+
function stopAutonomousWork(session) { return stopAutonomous(session, rolesFor(session), turnControl, taskControl, workerPermission); }
|
|
348
343
|
function beginHumanTurn(session) {
|
|
349
344
|
var resumed = turnControl.beginHumanTurn(session);
|
|
350
345
|
var roles = resumed && rolesFor(session);
|
|
351
346
|
if (roles) taskControl.drain(roles.driver, roles.worker);
|
|
352
347
|
return resumed;
|
|
353
348
|
}
|
|
349
|
+
function stopBarrier(session) { return turnControl.blockedReason(session); }
|
|
354
350
|
function getToolDefs(boundSession) {
|
|
355
351
|
if (!boundSession) return pairMcp.getToolDefs({
|
|
356
352
|
send: function () { return toolError(new Error("send_to_partner requires a session-bound tool server")); },
|
|
@@ -413,7 +409,8 @@ function attachSessionPair(ctx) {
|
|
|
413
409
|
}
|
|
414
410
|
var factory = attachPairFactory(ctx), createPairRecord = factory.createPairRecord,
|
|
415
411
|
createWorkerForDriver = factory.createWorkerForDriver, createPair = factory.createPair,
|
|
416
|
-
preflightWorkerForDriver = factory.preflightWorkerForDriver
|
|
412
|
+
preflightWorkerForDriver = factory.preflightWorkerForDriver, createOwnedPair = factory.createOwnedPair, removeOwnedPair = factory.removeOwnedPair,
|
|
413
|
+
preflightRuntime = factory.preflightRuntime;
|
|
417
414
|
lifecycle = attachPairLifecycle({
|
|
418
415
|
sm: sm,
|
|
419
416
|
splitStore: store,
|
|
@@ -490,8 +487,8 @@ function attachSessionPair(ctx) {
|
|
|
490
487
|
}
|
|
491
488
|
return pairPrompt;
|
|
492
489
|
}
|
|
493
|
-
return { beginHumanTurn: beginHumanTurn, getToolDefs: getToolDefs, handleHumanStop: handleHumanStop,
|
|
490
|
+
return { beginHumanTurn: beginHumanTurn, getToolDefs: getToolDefs, handleHumanStop: handleHumanStop, stopOwnedPair: stopOwnedPair, stopAutonomousWork: stopAutonomousWork, stopBarrier: stopBarrier,
|
|
494
491
|
handleMessage: handleMessage, handleTurnDone: handleTurnDone, getSystemPrompt: getSystemPrompt,
|
|
495
|
-
respondToWorkerProposal: workerProposal.respondToProposal, workerPermission: workerPermission
|
|
496
|
-
}
|
|
497
|
-
module.exports = { attachSessionPair: attachSessionPair, responseText: responseText, recentTurns: recentTurns };
|
|
492
|
+
respondToWorkerProposal: workerProposal.respondToProposal, workerPermission: workerPermission, createOwnedPair: createOwnedPair, removeOwnedPair: removeOwnedPair, preflightRuntime: preflightRuntime,
|
|
493
|
+
sendToPartner: sendToPartner, rolesFor: rolesFor };
|
|
494
|
+
} module.exports = { attachSessionPair: attachSessionPair, responseText: responseText, recentTurns: recentTurns };
|