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,35 @@
|
|
|
1
|
+
var MAX_CONTEXT = 12000;
|
|
2
|
+
|
|
3
|
+
function bounded(value) {
|
|
4
|
+
return typeof value === "string" ? value.substring(0, MAX_CONTEXT) : "";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function commonGuidance() {
|
|
8
|
+
return "Begin by clarifying any missing goal, scope, constraints, or target files. Inspect the relevant project code and tests before drafting. Produce a self-contained, goal-oriented brief that names the intended outcome, boundaries, and concrete evidence-based completion checks; every check must identify an observable artifact, behavior, or test result, never vague quality language. Review the brief with the user and refine it before any Start or file write. Do not claim completion without the stated evidence.";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function structuredQuestionGuidance() {
|
|
12
|
+
return "When clarification is needed, prefer the available structured question capability: AskUserQuestion, request_user_input, or Clay ask_user_questions (including its namespaced fallback). Ask only the minimum necessary, at most 1-3 concise questions per turn; provide 2-6 choices for bounded decisions and use a freeform question when choices would constrain the answer. Do not repeat information already established. Use plain chat only when no structured question capability is available.";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function interviewPrompt() {
|
|
16
|
+
return commonGuidance() + " " + structuredQuestionGuidance() + " Use the current chat and repository context. This is the built-in interactive Loop interview: after review, call propose_loop with the bounded objective, criteria, and limits. Do not execute, arm permissions, invoke /clay-ralph, require skills, use plan mode, create PROMPT.md/JUDGE.md, or start a run; wait for the human Start action.";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function legacyCraftPrompt(options) {
|
|
20
|
+
var opts = options || {};
|
|
21
|
+
var task = bounded(opts.task);
|
|
22
|
+
var directory = bounded(opts.directory);
|
|
23
|
+
var filePlan = opts.simple ? "Create only PROMPT.md; this Simple Loop has no judge." : opts.userJudge ? "Create only PROMPT.md; preserve the user's JUDGE.md and do not replace or edit it." : opts.judgeOnly ? "Create only JUDGE.md for the existing PROMPT.md; do not create or modify PROMPT.md." : "Create PROMPT.md and, when this is a judged Loop, JUDGE.md.";
|
|
24
|
+
return commonGuidance() + " " + structuredQuestionGuidance() + " This is legacy scheduled Loop file crafting. " +
|
|
25
|
+
"The files are the durable handoff to a future fresh session: that session starts from the target directory and reads the files from disk, so make each file self-contained and do not rely on this chat's transient context. " +
|
|
26
|
+
"Respect any user-provided files and the target directory; inspect existing files before writing and never overwrite a user-provided file. " + filePlan + " " +
|
|
27
|
+
"A separate judge is applicable only for judged Loops; it evaluates observable evidence from the fresh execution session and must not perform the task itself. Never make an automatic commit.\n\n" +
|
|
28
|
+
"## Task\n" + task + "\n\n## Loop Directory\n" + directory;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
commonGuidance: commonGuidance,
|
|
33
|
+
interviewPrompt: interviewPrompt,
|
|
34
|
+
legacyCraftPrompt: legacyCraftPrompt,
|
|
35
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
var recordRevision = require("./record-revision");
|
|
2
|
+
|
|
3
|
+
function attachLoopRegistryOccurrence(ctx) {
|
|
4
|
+
function normalize(id, now) {
|
|
5
|
+
var rec = ctx.getById(id);
|
|
6
|
+
if (!rec || !rec.enabled || !rec.scheduleRule || !rec.nextRunAt || rec.nextRunAt > now - 60000) return rec;
|
|
7
|
+
var next = ctx.nextRecordRunTime(rec, now - 60000);
|
|
8
|
+
if (Number(next) === Number(rec.nextRunAt)) return rec;
|
|
9
|
+
var previous = JSON.parse(JSON.stringify(rec));
|
|
10
|
+
rec.nextRunAt = next;
|
|
11
|
+
if (!next) rec.enabled = false;
|
|
12
|
+
if (!ctx.save()) { recordRevision.restoreRecord(rec, previous); return null; }
|
|
13
|
+
ctx.changed();
|
|
14
|
+
return rec;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function markNeedsOwner(id) {
|
|
18
|
+
var rec = ctx.getById(id);
|
|
19
|
+
if (!rec || rec.needsOwner === true) return false;
|
|
20
|
+
var previous = JSON.parse(JSON.stringify(rec));
|
|
21
|
+
rec.needsOwner = true;
|
|
22
|
+
rec.setupRequired = "owner";
|
|
23
|
+
rec.enabled = false;
|
|
24
|
+
rec.nextRunAt = null;
|
|
25
|
+
rec.scheduleEpoch = Math.max(1, Number(rec.scheduleEpoch) || 1) + 1;
|
|
26
|
+
rec.updatedAt = recordRevision.nextRevision(rec);
|
|
27
|
+
if (!ctx.save()) { recordRevision.restoreRecord(rec, previous); return false; }
|
|
28
|
+
ctx.changed();
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function prepare(id, occurrenceAt) {
|
|
33
|
+
var rec = ctx.getById(id);
|
|
34
|
+
if (!rec || !rec.enabled || Number(rec.nextRunAt) !== Number(occurrenceAt)) return { ok: false, error: "The scheduled occurrence is stale." };
|
|
35
|
+
var previous = JSON.parse(JSON.stringify(rec));
|
|
36
|
+
var prepared = ctx.prepareTrigger ? ctx.prepareTrigger(rec) : null;
|
|
37
|
+
if (prepared && prepared.ok === false) return prepared;
|
|
38
|
+
ctx.scheduledRuns.prepareRecord(rec, prepared);
|
|
39
|
+
if (!ctx.save()) { recordRevision.restoreRecord(rec, previous); return { ok: false, error: ctx.lastSaveError() || "The occurrence reservation could not be stored." }; }
|
|
40
|
+
ctx.changed();
|
|
41
|
+
return { ok: true, value: prepared };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function trigger(id, prepared) {
|
|
45
|
+
var rec = ctx.getById(id);
|
|
46
|
+
if (!rec) throw new Error("The scheduled record was deleted.");
|
|
47
|
+
if (ctx.onTrigger) ctx.onTrigger(rec, prepared);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function advance(id, occurrenceAt, result) {
|
|
51
|
+
var rec = ctx.getById(id);
|
|
52
|
+
if (!rec || Number(rec.nextRunAt) !== Number(occurrenceAt)) return false;
|
|
53
|
+
var previous = JSON.parse(JSON.stringify(rec));
|
|
54
|
+
var now = Date.now();
|
|
55
|
+
rec.lastRunAt = result === "interrupted" ? occurrenceAt : now;
|
|
56
|
+
if (result) rec.lastRunResult = result;
|
|
57
|
+
if (rec.scheduleRule) ctx.scheduleRule.recordScheduleStartState(rec, occurrenceAt);
|
|
58
|
+
if (rec.cron || rec.scheduleRule) {
|
|
59
|
+
rec.nextRunAt = ctx.nextRecordRunTime(rec, now);
|
|
60
|
+
if (rec.scheduleRule && !rec.nextRunAt) rec.enabled = false;
|
|
61
|
+
} else {
|
|
62
|
+
rec.nextRunAt = null;
|
|
63
|
+
rec.enabled = false;
|
|
64
|
+
}
|
|
65
|
+
if (!ctx.save()) { recordRevision.restoreRecord(rec, previous); return false; }
|
|
66
|
+
ctx.changed();
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function complete(id, occurrenceAt) { return advance(id, occurrenceAt, null); }
|
|
71
|
+
function reconcile(id, occurrenceAt) { return advance(id, occurrenceAt, "interrupted"); }
|
|
72
|
+
|
|
73
|
+
return { normalize: normalize, markNeedsOwner: markNeedsOwner, prepare: prepare, trigger: trigger, complete: complete, reconcile: reconcile };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { attachLoopRegistryOccurrence: attachLoopRegistryOccurrence };
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
var crypto = require("crypto");
|
|
2
|
+
var driverEligibility = require("./session-driver-eligibility");
|
|
3
|
+
var attachWakeScheduler = require("./autonomous-run-scheduler").attachAutonomousRunScheduler;
|
|
4
|
+
var runRecord = require("./autonomous-run-record");
|
|
5
|
+
|
|
6
|
+
var DEFAULT_CONTINUATIONS = 10;
|
|
7
|
+
var DEFAULT_MINUTES = 60;
|
|
8
|
+
var ACTIVE_STATES = ["armed", "running", "reviewing", "waiting-worker", "waiting-user", "paused"];
|
|
9
|
+
var TERMINAL_STATES = ["completed", "stopped", "limit", "error"];
|
|
10
|
+
function cleanText(value, max) {
|
|
11
|
+
return typeof value === "string" ? value.trim().substring(0, max) : "";
|
|
12
|
+
}
|
|
13
|
+
function boundedInt(value, fallback, max) {
|
|
14
|
+
var number = Number(value);
|
|
15
|
+
return Number.isFinite(number) ? Math.max(1, Math.min(max, Math.floor(number))) : fallback;
|
|
16
|
+
}
|
|
17
|
+
function cleanCriteria(value) {
|
|
18
|
+
var criteria = Array.isArray(value) ? value : [];
|
|
19
|
+
return criteria.slice(0, 10).map(function (item) { return cleanText(item, 1000); }).filter(Boolean);
|
|
20
|
+
}
|
|
21
|
+
var project = runRecord.project;
|
|
22
|
+
var restoreStoredRun = runRecord.restore;
|
|
23
|
+
function attachAutonomousRun(ctx) {
|
|
24
|
+
var sm = ctx.sm;
|
|
25
|
+
var sendTo = ctx.sendTo;
|
|
26
|
+
var fullAccess = ctx.fullAccess;
|
|
27
|
+
var timers = new Map();
|
|
28
|
+
var actions = new Map();
|
|
29
|
+
var wakeScheduler = null;
|
|
30
|
+
|
|
31
|
+
function serialize(session, action) {
|
|
32
|
+
if (!session) return Promise.reject(new Error("No session was bound to this request."));
|
|
33
|
+
var prior = actions.get(session.localId) || Promise.resolve();
|
|
34
|
+
var next = prior.catch(function () {}).then(action);
|
|
35
|
+
actions.set(session.localId, next);
|
|
36
|
+
return next.finally(function () { if (actions.get(session.localId) === next) actions.delete(session.localId); });
|
|
37
|
+
}
|
|
38
|
+
function broadcast(session) {
|
|
39
|
+
sm.sendToSession(session, { type: "autonomous_run_state", sessionId: session.localId, run: project(session.autonomousRun) });
|
|
40
|
+
sm.broadcastSessionList();
|
|
41
|
+
}
|
|
42
|
+
function persist(session) { sm.saveSessionFile(session); broadcast(session); }
|
|
43
|
+
function clearTimer(session) {
|
|
44
|
+
if (wakeScheduler && session && session.autonomousRun) {
|
|
45
|
+
try { wakeScheduler.cancel(session, session.autonomousRun); }
|
|
46
|
+
catch (error) { session.autonomousRun.schedulerError = cleanText(error.message || String(error), 1000); }
|
|
47
|
+
}
|
|
48
|
+
var timer = timers.get(session.localId);
|
|
49
|
+
if (timer) clearTimeout(timer);
|
|
50
|
+
timers.delete(session.localId);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function scheduleLimit(session) {
|
|
54
|
+
var run = session.autonomousRun;
|
|
55
|
+
if (!run || TERMINAL_STATES.indexOf(run.state) !== -1 || run.state === "paused" || !run.deadlineAt) return;
|
|
56
|
+
var remaining = run.deadlineAt - Date.now();
|
|
57
|
+
if (remaining <= 0) { finish(session, "limit", "The wall-clock limit was reached."); return; }
|
|
58
|
+
if (wakeScheduler) { wakeScheduler.schedule(session, run, "deadline", run.deadlineAt); return; }
|
|
59
|
+
clearTimer(session);
|
|
60
|
+
timers.set(session.localId, setTimeout(function () { finish(session, "limit", "The wall-clock limit was reached."); }, Math.min(remaining, 2147483647)));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function eligible(session) {
|
|
64
|
+
return !ctx.isMate && !!session && (session.runtimeMode || session.mode) === "gui" && driverEligibility.isEligibleDriverSession(session);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function owns(ws, session, msg) {
|
|
68
|
+
if (!session || !msg || Number(msg.sessionId) !== Number(session.localId)) return false;
|
|
69
|
+
return !ctx.canAccess || ctx.canAccess(ws, session);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function ownsRun(session, msg) {
|
|
73
|
+
return !!(session && session.autonomousRun && cleanText(msg && msg.runId, 200) === session.autonomousRun.id);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function removeRunOwnedPending(session, runId) {
|
|
77
|
+
if (!Array.isArray(session.pendingPush)) return;
|
|
78
|
+
session.pendingPush = session.pendingPush.filter(function (item) { return item.autonomousRunId !== runId; });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function finishNow(session, state, reason, outcome) {
|
|
82
|
+
var run = session && session.autonomousRun;
|
|
83
|
+
if (!run || TERMINAL_STATES.indexOf(run.state) !== -1) return false;
|
|
84
|
+
run.cancellationEpoch = (run.cancellationEpoch || 0) + 1;
|
|
85
|
+
run.continuationPending = false;
|
|
86
|
+
run.completionRequested = false;
|
|
87
|
+
run.state = state;
|
|
88
|
+
run.terminalReason = cleanText(reason, 2000);
|
|
89
|
+
run.finishedAt = Date.now();
|
|
90
|
+
if (outcome) run.outcome = outcome;
|
|
91
|
+
clearTimer(session);
|
|
92
|
+
removeRunOwnedPending(session, run.id);
|
|
93
|
+
if (typeof ctx.stopPair === "function") ctx.stopPair(session);
|
|
94
|
+
if (state !== "completed" && (session.isProcessing || session._queryStarting)) {
|
|
95
|
+
session.taskStopRequested = true;
|
|
96
|
+
if (session.abortController) session.abortController.abort();
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
await fullAccess.restore(session, run.permissionSnapshot);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
run.permissionRestoreError = cleanText(error.message || String(error), 1000);
|
|
102
|
+
run.state = "error";
|
|
103
|
+
run.terminalReason = "The run ended, but Skip Permissions could not be restored: " + run.permissionRestoreError;
|
|
104
|
+
}
|
|
105
|
+
persist(session);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function finish(session, state, reason, outcome) {
|
|
110
|
+
return serialize(session, function () { return finishNow(session, state, reason, outcome); });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function finishOwned(session, runId, state, reason, outcome) {
|
|
114
|
+
return serialize(session, function () {
|
|
115
|
+
if (!session.autonomousRun || session.autonomousRun.id !== runId) return false;
|
|
116
|
+
return finishNow(session, state, reason, outcome);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function armNow(ws, session, msg, options) {
|
|
121
|
+
if (!owns(ws, session, msg)) throw new Error("The requested session is not authorized.");
|
|
122
|
+
if (!eligible(session)) throw new Error("Until complete is only available in a project GUI Driver session.");
|
|
123
|
+
if (session.isProcessing || session._queryStarting) throw new Error("Wait for the current task to finish before selecting Until complete.");
|
|
124
|
+
var existing = session.autonomousRun;
|
|
125
|
+
if (existing && ACTIVE_STATES.indexOf(existing.state) !== -1) throw new Error("This session already has an active Until complete run.");
|
|
126
|
+
var permissionSnapshot = fullAccess.snapshot(session);
|
|
127
|
+
await fullAccess.setEnabled(session, true);
|
|
128
|
+
try {
|
|
129
|
+
var run = {
|
|
130
|
+
version: 1, id: crypto.randomUUID(), armToken: crypto.randomUUID(), state: "armed",
|
|
131
|
+
objective: "", successCriteria: cleanCriteria(msg.successCriteria), continuationCount: 0,
|
|
132
|
+
maxContinuations: boundedInt(msg.maxContinuations, DEFAULT_CONTINUATIONS, 100),
|
|
133
|
+
maxMinutes: boundedInt(msg.maxMinutes, DEFAULT_MINUTES, 1440),
|
|
134
|
+
permissionSnapshot: permissionSnapshot, createdAt: Date.now(), cancellationEpoch: 0,
|
|
135
|
+
continuationPending: false, wakeSequence: 0,
|
|
136
|
+
};
|
|
137
|
+
if (run.successCriteria.length === 0) run.successCriteria = ["The requested outcome is implemented and meaningfully verified."];
|
|
138
|
+
session.autonomousRun = run;
|
|
139
|
+
persist(session);
|
|
140
|
+
if (!options || !options.deferResult) sendTo(ws, { type: "autonomous_run_arm_result", ok: true, sessionId: session.localId, requestId: cleanText(msg.requestId, 200), runId: run.id, armToken: run.armToken, run: project(run) });
|
|
141
|
+
} catch (error) {
|
|
142
|
+
session.autonomousRun = null;
|
|
143
|
+
await fullAccess.restore(session, permissionSnapshot);
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function disarmNow(ws, session, msg) {
|
|
149
|
+
if (!owns(ws, session, msg)) throw new Error("The requested session is not authorized.");
|
|
150
|
+
var run = session.autonomousRun;
|
|
151
|
+
if (!run || run.state !== "armed") throw new Error("There is no armed Until complete task to deselect.");
|
|
152
|
+
run.cancellationEpoch = (run.cancellationEpoch || 0) + 1;
|
|
153
|
+
await fullAccess.restore(session, run.permissionSnapshot);
|
|
154
|
+
session.autonomousRun = null;
|
|
155
|
+
persist(session);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function consume(session, msg) {
|
|
159
|
+
var run = session && session.autonomousRun;
|
|
160
|
+
if (!run || session._autonomousLifecyclePending || !eligible(session) || run.state !== "armed" || msg.autonomousRunToken !== run.armToken) return false;
|
|
161
|
+
run.state = "running";
|
|
162
|
+
run.objective = cleanText(msg.text, 20000) || "Complete the submitted task.";
|
|
163
|
+
if (run.successCriteria.length === 0) run.successCriteria = ["The requested outcome is implemented and meaningfully verified."];
|
|
164
|
+
run.startedAt = Date.now();
|
|
165
|
+
run.deadlineAt = run.startedAt + run.maxMinutes * 60000;
|
|
166
|
+
delete run.armToken;
|
|
167
|
+
persist(session);
|
|
168
|
+
try { scheduleLimit(session); } catch (error) { finish(session, "error", "The run deadline could not be stored: " + (error.message || error)); return false; }
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function startApprovedBrief(ws, session, brief, requestId, isStillApproved) {
|
|
173
|
+
return serialize(session, async function () {
|
|
174
|
+
if (!owns(ws, session, { sessionId: session && session.localId }) || !eligible(session)) throw new Error("The requested session is not authorized.");
|
|
175
|
+
if (session.loopInterviewHandoff && session.loopInterviewHandoff.requestId === cleanText(requestId, 200) && session.autonomousRun) {
|
|
176
|
+
sendTo(ws, { type: "loop_interview_start_run_result", ok: true, duplicate: true, sessionId: session.localId, requestId: cleanText(requestId, 200), run: project(session.autonomousRun) });
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
if (session.isProcessing || session._queryStarting) throw new Error("Wait for the current task to finish before starting the Loop run.");
|
|
180
|
+
if (session.autonomousRun && ACTIVE_STATES.indexOf(session.autonomousRun.state) !== -1) throw new Error("This session already has an active Until complete run.");
|
|
181
|
+
session.loopInterviewHandoff = { version: 1, proposalId: brief.id, proposalVersion: brief.version, requestId: cleanText(requestId, 200), state: "starting", createdAt: Date.now() };
|
|
182
|
+
sm.saveSessionFile(session);
|
|
183
|
+
try {
|
|
184
|
+
await armNow(ws, session, { sessionId: session.localId, requestId: requestId, successCriteria: brief.successCriteria, maxContinuations: brief.maxContinuations, maxMinutes: brief.maxMinutes }, { deferResult: true });
|
|
185
|
+
if (!owns(ws, session, { sessionId: session.localId }) || !eligible(session) || session.isProcessing || session._queryStarting) throw new Error("The session changed while permissions were being prepared.");
|
|
186
|
+
if (typeof isStillApproved === "function" && !isStillApproved()) throw new Error("This Loop brief was changed or cancelled while permissions were being prepared.");
|
|
187
|
+
var run = session.autonomousRun;
|
|
188
|
+
if (!run || !run.armToken || !consume(session, { text: brief.objective, autonomousRunToken: run.armToken })) throw new Error("The approved Loop brief could not be consumed.");
|
|
189
|
+
var entry = { type: "user_message", text: "[Approved Loop run started]\n\n" + brief.objective, source: "loop_interview_run", proposalId: brief.id, proposalVersion: brief.version, _ts: Date.now() };
|
|
190
|
+
session.history.push(entry);
|
|
191
|
+
sm.appendToSessionFile(session, entry);
|
|
192
|
+
sm.sendToSession(session, entry);
|
|
193
|
+
session.isProcessing = true;
|
|
194
|
+
ctx.onProcessingChanged();
|
|
195
|
+
sm.sendToSession(session, { type: "status", status: "processing" });
|
|
196
|
+
var sdk = ctx.getSdk();
|
|
197
|
+
session.loopInterviewHandoff.state = "running";
|
|
198
|
+
sm.saveSessionFile(session);
|
|
199
|
+
var started;
|
|
200
|
+
try { started = sdk.startQuery(session, brief.objective, null, ctx.ensureProjectAccessForSession(session)); }
|
|
201
|
+
catch (error) {
|
|
202
|
+
session.isProcessing = false;
|
|
203
|
+
session._queryStarting = false;
|
|
204
|
+
ctx.onProcessingChanged();
|
|
205
|
+
sm.sendToSession(session, { type: "status", status: "idle" });
|
|
206
|
+
await finishNow(session, "error", "The approved Loop run could not start: " + (error.message || error));
|
|
207
|
+
session.loopInterviewHandoff = null;
|
|
208
|
+
sm.saveSessionFile(session);
|
|
209
|
+
sendTo(ws, { type: "loop_interview_start_run_result", ok: false, sessionId: session.localId, requestId: cleanText(requestId, 200), error: "The approved Loop run could not start: " + (error.message || error) });
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
sendTo(ws, { type: "loop_interview_start_run_result", ok: true, sessionId: session.localId, requestId: cleanText(requestId, 200), run: project(session.autonomousRun) });
|
|
213
|
+
var approvedRunId = session.autonomousRun.id;
|
|
214
|
+
Promise.resolve(started).catch(function (error) {
|
|
215
|
+
if (session.autonomousRun && session.autonomousRun.id === approvedRunId && TERMINAL_STATES.indexOf(session.autonomousRun.state) === -1) {
|
|
216
|
+
session.isProcessing = false;
|
|
217
|
+
session._queryStarting = false;
|
|
218
|
+
ctx.onProcessingChanged();
|
|
219
|
+
sm.sendToSession(session, { type: "status", status: "idle" });
|
|
220
|
+
finishOwned(session, approvedRunId, "error", "The approved Loop run could not start: " + (error.message || error));
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
return true;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (session.autonomousRun && ACTIVE_STATES.indexOf(session.autonomousRun.state) !== -1) await finishNow(session, "stopped", "The approved Loop handoff failed before execution could start.");
|
|
226
|
+
session.loopInterviewHandoff = null;
|
|
227
|
+
sm.saveSessionFile(session);
|
|
228
|
+
sendTo(ws, { type: "loop_interview_start_run_result", ok: false, sessionId: session.localId, requestId: cleanText(requestId, 200), error: error.message || String(error) });
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function onHumanMessage(session) {
|
|
235
|
+
var run = session && session.autonomousRun;
|
|
236
|
+
if (!run || run.state === "armed" || run.state === "paused" || TERMINAL_STATES.indexOf(run.state) !== -1) return;
|
|
237
|
+
run.completionRequested = false;
|
|
238
|
+
run.outcome = null;
|
|
239
|
+
run.state = "running";
|
|
240
|
+
run.waitingReason = "";
|
|
241
|
+
persist(session);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function continuationPrompt(run) {
|
|
245
|
+
return "[Until complete continuation: " + run.id + "]\nContinue the single active task. Review the objective and success criteria, inspect actual evidence, and do the next useful work. Do not claim independent verification merely from your own prose. Before ending this turn, call report_until_complete_outcome with this runId and a structured status, summary, and concrete evidence. If a Worker or human response is required, report the corresponding waiting status and do not spin.";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function dispatchContinuation(session, runId, epoch) {
|
|
249
|
+
var run = session.autonomousRun;
|
|
250
|
+
if (!run || run.id !== runId || run.cancellationEpoch !== epoch || !run.continuationPending || TERMINAL_STATES.indexOf(run.state) !== -1) return;
|
|
251
|
+
if (Date.now() >= run.deadlineAt) { finish(session, "limit", "The wall-clock limit was reached."); return; }
|
|
252
|
+
if (run.rateLimitUntil && run.rateLimitUntil > Date.now()) { scheduleRateLimitWake(session, run); return; }
|
|
253
|
+
if (typeof ctx.stopBarrier === "function" && ctx.stopBarrier(session)) { finish(session, "stopped", "A cancellation barrier prevented a stale continuation."); return; }
|
|
254
|
+
if (session.isProcessing || session._queryStarting) {
|
|
255
|
+
run.continuationPending = false;
|
|
256
|
+
run.state = "reviewing";
|
|
257
|
+
persist(session);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
run.continuationPending = false;
|
|
261
|
+
run.continuationCount += 1;
|
|
262
|
+
run.state = "running";
|
|
263
|
+
session.isProcessing = true;
|
|
264
|
+
session.taskStopRequested = false;
|
|
265
|
+
ctx.onProcessingChanged();
|
|
266
|
+
sm.sendToSession(session, { type: "status", status: "processing" });
|
|
267
|
+
var text = continuationPrompt(run);
|
|
268
|
+
var sdk = ctx.getSdk();
|
|
269
|
+
try {
|
|
270
|
+
if (!sdk.pushMessage(session, text, null, { autonomousRunId: run.id })) {
|
|
271
|
+
var started = sdk.startQuery(session, text, null, ctx.ensureProjectAccessForSession(session));
|
|
272
|
+
var queryOwner = { session: session, runId: run.id, epoch: epoch, generation: Number(session._queryGeneration || 0) };
|
|
273
|
+
session._autonomousQueryOwner = queryOwner;
|
|
274
|
+
Promise.resolve(started).then(function () {
|
|
275
|
+
if (session._autonomousQueryOwner === queryOwner) delete session._autonomousQueryOwner;
|
|
276
|
+
}, function (error) {
|
|
277
|
+
var current = sm.sessions.get(session.localId);
|
|
278
|
+
var currentRun = session.autonomousRun;
|
|
279
|
+
if (current !== session || session._autonomousQueryOwner !== queryOwner || !currentRun || currentRun.id !== queryOwner.runId ||
|
|
280
|
+
Number(currentRun.cancellationEpoch || 0) !== queryOwner.epoch || Number(session._queryGeneration || 0) !== queryOwner.generation) return;
|
|
281
|
+
delete session._autonomousQueryOwner;
|
|
282
|
+
finish(session, "error", "Failed to continue the run: " + (error.message || error));
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
} catch (error) {
|
|
286
|
+
finish(session, "error", "Failed to continue the run: " + (error.message || error));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
persist(session);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function scheduleRateLimitWake(session, run) {
|
|
293
|
+
var wakeAt = Math.min(run.rateLimitUntil, run.deadlineAt);
|
|
294
|
+
var runId = run.id;
|
|
295
|
+
var epoch = run.cancellationEpoch;
|
|
296
|
+
if (wakeScheduler) {
|
|
297
|
+
wakeScheduler.cancel(session, run);
|
|
298
|
+
wakeScheduler.schedule(session, run, "deadline", run.deadlineAt);
|
|
299
|
+
wakeScheduler.schedule(session, run, "rate-limit", wakeAt);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
clearTimer(session);
|
|
303
|
+
timers.set(session.localId, setTimeout(function () {
|
|
304
|
+
var current = session.autonomousRun;
|
|
305
|
+
if (!current || current.id !== runId || current.cancellationEpoch !== epoch || TERMINAL_STATES.indexOf(current.state) !== -1) return;
|
|
306
|
+
if (Date.now() >= current.deadlineAt) { finish(session, "limit", "The wall-clock limit was reached."); return; }
|
|
307
|
+
current.rateLimitUntil = null;
|
|
308
|
+
current.waitingReason = "";
|
|
309
|
+
scheduleLimit(session);
|
|
310
|
+
continueRun(session, current);
|
|
311
|
+
}, Math.max(0, wakeAt - Date.now())));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function continueRun(session, run) {
|
|
315
|
+
if (run.continuationPending || session.isProcessing || session._queryStarting) return;
|
|
316
|
+
if (Date.now() >= run.deadlineAt) { finish(session, "limit", "The wall-clock limit was reached."); return; }
|
|
317
|
+
if (run.rateLimitUntil && run.rateLimitUntil > Date.now()) { scheduleRateLimitWake(session, run); return; }
|
|
318
|
+
if (run.continuationCount >= run.maxContinuations) { finish(session, "limit", "The automatic continuation limit was reached."); return; }
|
|
319
|
+
run.continuationPending = true;
|
|
320
|
+
run.state = "reviewing";
|
|
321
|
+
var epoch = run.cancellationEpoch;
|
|
322
|
+
persist(session);
|
|
323
|
+
if (wakeScheduler) {
|
|
324
|
+
try { wakeScheduler.schedule(session, run, "continuation", Date.now()); }
|
|
325
|
+
catch (error) { finish(session, "error", "The continuation could not be stored: " + (error.message || error)); }
|
|
326
|
+
} else setTimeout(function () { dispatchContinuation(session, run.id, epoch); }, 0);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function onTurnDone(session) {
|
|
330
|
+
var run = session && session.autonomousRun;
|
|
331
|
+
if (!run || run.state === "armed" || run.state === "paused" || TERMINAL_STATES.indexOf(run.state) !== -1) return;
|
|
332
|
+
if (run.completionRequested) { finish(session, "completed", "The Driver assessed the success criteria as complete.", run.outcome); return; }
|
|
333
|
+
if (run.state === "waiting-user" || run.state === "waiting-worker") return;
|
|
334
|
+
continueRun(session, run);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function report(args, session) {
|
|
338
|
+
var run = session && session.autonomousRun;
|
|
339
|
+
if (!session || sm.sessions.get(session.localId) !== session || !run || args.runId !== run.id || run.state === "armed" || run.state === "paused" || TERMINAL_STATES.indexOf(run.state) !== -1) return Promise.resolve({ content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: "This callback does not belong to a running active run." }) }] });
|
|
340
|
+
var statuses = ["completed", "continue", "waiting_user", "waiting_worker", "error"];
|
|
341
|
+
if (statuses.indexOf(args.status) === -1) return Promise.resolve({ content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: "Invalid outcome status." }) }] });
|
|
342
|
+
var evidence = Array.isArray(args.evidence) ? args.evidence.slice(0, 20).map(function (item) { return cleanText(item, 2000); }).filter(Boolean) : [];
|
|
343
|
+
var criteria = Array.isArray(args.criteria) ? args.criteria.slice(0, 10).map(function (item) {
|
|
344
|
+
if (!item || typeof item !== "object") return null;
|
|
345
|
+
return { criterion: cleanText(item.criterion, 1000), met: item.met === true, evidence: cleanText(item.evidence, 2000) };
|
|
346
|
+
}).filter(function (item) { return item && item.criterion && item.evidence; }) : [];
|
|
347
|
+
var outcome = { assessment: args.status, summary: cleanText(args.summary, 4000), evidence: evidence, criteria: criteria, reportedAt: Date.now() };
|
|
348
|
+
var criteriaComplete = (run.successCriteria || []).every(function (criterion) {
|
|
349
|
+
return criteria.some(function (item) { return item.met && item.criterion === criterion; });
|
|
350
|
+
});
|
|
351
|
+
if (args.status === "completed" && (!outcome.summary || evidence.length === 0 || !run.successCriteria || run.successCriteria.length === 0 || !criteriaComplete)) return Promise.resolve({ content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: "Completion requires a summary, concrete evidence, and a met assessment for every success criterion." }) }] });
|
|
352
|
+
run.completionRequested = false;
|
|
353
|
+
run.outcome = outcome;
|
|
354
|
+
if (args.status === "completed") { run.completionRequested = true; run.state = "reviewing"; persist(session); }
|
|
355
|
+
else if (args.status === "error") finish(session, "error", outcome.summary || "The Driver reported an error.", outcome);
|
|
356
|
+
else if (args.status === "waiting_user") { run.state = "waiting-user"; run.waitingReason = outcome.summary || "Waiting for human input."; persist(session); }
|
|
357
|
+
else if (args.status === "waiting_worker") { run.state = "waiting-worker"; run.waitingReason = outcome.summary || "Waiting for the Split Worker."; persist(session); }
|
|
358
|
+
else { run.state = "reviewing"; run.waitingReason = ""; persist(session); }
|
|
359
|
+
return Promise.resolve({ content: [{ type: "text", text: JSON.stringify({ status: "recorded", run: project(run) }) }] });
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function getToolDefs(session) {
|
|
363
|
+
var run = session && session.autonomousRun;
|
|
364
|
+
if (!run || run.state === "armed" || run.state === "paused" || TERMINAL_STATES.indexOf(run.state) !== -1) return [];
|
|
365
|
+
return [{ name: "report_until_complete_outcome", description: "Report the Driver's structured assessment for the exact active Until complete run.", inputSchema: { type: "object", properties: { runId: { type: "string" }, status: { type: "string", enum: ["completed", "continue", "waiting_user", "waiting_worker", "error"] }, summary: { type: "string" }, evidence: { type: "array", items: { type: "string" } }, criteria: { type: "array", items: { type: "object", properties: { criterion: { type: "string" }, met: { type: "boolean" }, evidence: { type: "string" } }, required: ["criterion", "met", "evidence"], additionalProperties: false } } }, required: ["runId", "status", "summary", "evidence", "criteria"], additionalProperties: false }, handler: function (args) { return report(args || {}, session); } }];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function getSystemPrompt(session) {
|
|
369
|
+
var run = session && session.autonomousRun;
|
|
370
|
+
if (!run || run.state === "armed" || TERMINAL_STATES.indexOf(run.state) !== -1) return "";
|
|
371
|
+
return "An Until complete run is active. Run ID: " + run.id + ". Objective: " + run.objective + ". Success criteria: " + run.successCriteria.join("; ") + ". You are the Driver and may use a Split Worker when useful. Base completion only on inspected evidence, not your own prose. Before ending every turn, call report_until_complete_outcome with a criterion-by-criterion assessment. Report waiting_user or waiting_worker when blocked and wait rather than spinning.";
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function stopForPermissionChange(session) { return finish(session, "stopped", "Skip Permissions was turned off."); }
|
|
375
|
+
|
|
376
|
+
function handleMessage(ws, msg) {
|
|
377
|
+
if (msg.type !== "autonomous_run_arm" && msg.type !== "autonomous_run_disarm" && msg.type !== "autonomous_run_stop" && msg.type !== "autonomous_run_resume") return false;
|
|
378
|
+
var session = ctx.getSessionForWs(ws);
|
|
379
|
+
var requestId = cleanText(msg.requestId, 200);
|
|
380
|
+
var runId = cleanText(msg.runId, 200) || (session && session.autonomousRun && session.autonomousRun.id) || "";
|
|
381
|
+
if (!session) {
|
|
382
|
+
sendTo(ws, { type: msg.type === "autonomous_run_arm" ? "autonomous_run_arm_result" : "autonomous_run_action_result", ok: false, sessionId: null, requestId: requestId, runId: runId, error: "No session was bound to this request." });
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
var lifecycle = null;
|
|
386
|
+
if (msg.type !== "autonomous_run_arm" && owns(ws, session, msg) && ownsRun(session, msg)) {
|
|
387
|
+
lifecycle = { id: crypto.randomUUID(), type: msg.type, runId: runId };
|
|
388
|
+
session._autonomousLifecyclePending = lifecycle;
|
|
389
|
+
}
|
|
390
|
+
serialize(session, async function () {
|
|
391
|
+
try {
|
|
392
|
+
if (msg.type === "autonomous_run_arm") await armNow(ws, session, msg);
|
|
393
|
+
else if (!owns(ws, session, msg) || !ownsRun(session, msg)) throw new Error("The requested run is stale or is not authorized.");
|
|
394
|
+
else if (msg.type === "autonomous_run_disarm") await disarmNow(ws, session, msg);
|
|
395
|
+
else if (msg.type === "autonomous_run_stop") await finishNow(session, "stopped", "Stopped by the user.");
|
|
396
|
+
else {
|
|
397
|
+
var run = session.autonomousRun;
|
|
398
|
+
if (!run || run.state !== "paused") throw new Error("There is no paused run to resume.");
|
|
399
|
+
if (Date.now() >= run.deadlineAt) { await finishNow(session, "limit", "The wall-clock limit was reached."); return; }
|
|
400
|
+
await fullAccess.setEnabled(session, true);
|
|
401
|
+
run.state = "reviewing";
|
|
402
|
+
run.pausedAfterRestart = false;
|
|
403
|
+
run.waitingReason = "";
|
|
404
|
+
persist(session);
|
|
405
|
+
try { scheduleLimit(session); }
|
|
406
|
+
catch (error) {
|
|
407
|
+
await finishNow(session, "error", "The run deadline could not be stored: " + (error.message || error));
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
continueRun(session, run);
|
|
411
|
+
}
|
|
412
|
+
if (msg.type !== "autonomous_run_arm") sendTo(ws, { type: "autonomous_run_action_result", ok: true, action: msg.type, sessionId: session.localId, requestId: requestId, runId: runId });
|
|
413
|
+
} catch (error) {
|
|
414
|
+
sendTo(ws, { type: msg.type === "autonomous_run_arm" ? "autonomous_run_arm_result" : "autonomous_run_action_result", ok: false, action: msg.type, sessionId: session.localId, requestId: requestId, runId: runId, error: error.message || String(error) });
|
|
415
|
+
}
|
|
416
|
+
}).catch(function () {}).finally(function () {
|
|
417
|
+
if (lifecycle && session._autonomousLifecyclePending === lifecycle) delete session._autonomousLifecyclePending;
|
|
418
|
+
});
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function recover() {
|
|
423
|
+
sm.sessions.forEach(function (session) {
|
|
424
|
+
var run = session.autonomousRun;
|
|
425
|
+
if (!run || TERMINAL_STATES.indexOf(run.state) !== -1) return;
|
|
426
|
+
if (wakeScheduler) {
|
|
427
|
+
try { wakeScheduler.cancel(session, run); }
|
|
428
|
+
catch (error) { run.schedulerError = cleanText(error.message || String(error), 1000); }
|
|
429
|
+
}
|
|
430
|
+
if (run.state === "armed") {
|
|
431
|
+
serialize(session, function () {
|
|
432
|
+
return fullAccess.restore(session, run.permissionSnapshot).then(function () { session.autonomousRun = null; session.loopInterviewHandoff = null; persist(session); }, function (error) {
|
|
433
|
+
run.state = "error"; run.terminalReason = "Skip Permissions could not be restored after restart: " + cleanText(error.message || String(error), 1000); persist(session);
|
|
434
|
+
});
|
|
435
|
+
}).catch(function () {});
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
run.state = "paused";
|
|
439
|
+
run.pausedAfterRestart = true;
|
|
440
|
+
run.waitingReason = "Paused after daemon restart. Resume explicitly to continue.";
|
|
441
|
+
run.continuationPending = false;
|
|
442
|
+
run.cancellationEpoch = (run.cancellationEpoch || 0) + 1;
|
|
443
|
+
serialize(session, function () {
|
|
444
|
+
return fullAccess.restore(session, run.permissionSnapshot).catch(function (error) {
|
|
445
|
+
run.permissionRestoreError = cleanText(error.message || String(error), 1000);
|
|
446
|
+
run.state = "error"; run.terminalReason = "Skip Permissions could not be restored after restart: " + run.permissionRestoreError;
|
|
447
|
+
}).then(function () { persist(session); });
|
|
448
|
+
}).catch(function () {});
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function onPartnerResult(session) {
|
|
453
|
+
var run = session && session.autonomousRun;
|
|
454
|
+
if (!run || run.state !== "waiting-worker") return;
|
|
455
|
+
run.completionRequested = false; run.outcome = null;
|
|
456
|
+
run.state = "running"; run.waitingReason = ""; persist(session);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function onUserAnswer(session) {
|
|
460
|
+
var run = session && session.autonomousRun;
|
|
461
|
+
if (!run || run.state !== "waiting-user") return;
|
|
462
|
+
run.completionRequested = false; run.outcome = null;
|
|
463
|
+
run.state = "running"; run.waitingReason = ""; persist(session);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function onRateLimit(session, resetsAt) {
|
|
467
|
+
var run = session && session.autonomousRun;
|
|
468
|
+
if (!run || TERMINAL_STATES.indexOf(run.state) !== -1 || run.state === "armed" || run.state === "paused") return;
|
|
469
|
+
run.cancellationEpoch = (run.cancellationEpoch || 0) + 1;
|
|
470
|
+
run.continuationPending = false;
|
|
471
|
+
run.rateLimitUntil = Math.max(Date.now(), Math.min(Number(resetsAt) || Date.now(), run.deadlineAt));
|
|
472
|
+
run.state = "reviewing";
|
|
473
|
+
run.waitingReason = "Waiting for the rate limit to reset.";
|
|
474
|
+
persist(session);
|
|
475
|
+
try { scheduleRateLimitWake(session, run); }
|
|
476
|
+
catch (error) { finish(session, "error", "The rate-limit wake could not be stored: " + (error.message || error)); }
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (ctx.durableScheduler) {
|
|
480
|
+
wakeScheduler = attachWakeScheduler({
|
|
481
|
+
scheduler: ctx.durableScheduler, projectId: ctx.projectSlug, sm: sm, isMultiUser: ctx.isMultiUser,
|
|
482
|
+
authorize: ctx.authorizeSession,
|
|
483
|
+
onWake: function (kind, session, run) {
|
|
484
|
+
if (kind === "deadline") return finishOwned(session, run.id, "limit", "The wall-clock limit was reached.");
|
|
485
|
+
if (kind === "rate-limit") {
|
|
486
|
+
run.rateLimitUntil = null; run.waitingReason = ""; persist(session); scheduleLimit(session); continueRun(session, run); return true;
|
|
487
|
+
}
|
|
488
|
+
dispatchContinuation(session, run.id, run.cancellationEpoch); return true;
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
} else if (ctx.allowInMemoryTimers !== true) throw new Error("Until complete requires the durable scheduler.");
|
|
492
|
+
recover();
|
|
493
|
+
if (wakeScheduler) wakeScheduler.start();
|
|
494
|
+
return { consume: consume, finish: finish, startApprovedBrief: startApprovedBrief, getSystemPrompt: getSystemPrompt, getToolDefs: getToolDefs, handleMessage: handleMessage, onHumanMessage: onHumanMessage, onPartnerResult: onPartnerResult, onRateLimit: onRateLimit, onTurnDone: onTurnDone, onUserAnswer: onUserAnswer, project: project, stopForPermissionChange: stopForPermissionChange,
|
|
495
|
+
shutdown: function () { if (wakeScheduler) wakeScheduler.shutdown(); } };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
module.exports = { attachAutonomousRun: attachAutonomousRun, projectAutonomousRun: project, restoreStoredRun: restoreStoredRun };
|
|
@@ -95,6 +95,7 @@ function attachConnection(ctx) {
|
|
|
95
95
|
var getTitle = ctx.getTitle;
|
|
96
96
|
var getProject = ctx.getProject;
|
|
97
97
|
var warmup = ctx.warmup;
|
|
98
|
+
var scheduledMessages = ctx.scheduledMessages;
|
|
98
99
|
|
|
99
100
|
// Adapters are initialized lazily: the first websocket connection into
|
|
100
101
|
// this project triggers warmup. Without this guard we would either keep
|
|
@@ -262,7 +263,8 @@ function attachConnection(ctx) {
|
|
|
262
263
|
try { resolveSessionForView(active, ws); } catch (e) {}
|
|
263
264
|
}
|
|
264
265
|
var _vendorCaps = (sm.capabilitiesByVendor && sm.capabilitiesByVendor[active.vendor || sm.defaultVendor || "claude"]) || {};
|
|
265
|
-
sendTo(ws, { type: "session_switched", id: active.localId, cliSessionId: active.cliSessionId || null, loop: active.loop || null, vendor: active.vendor || null, model: active.model || null, effort: active.effort || initialEffort || null, hasHistory: (active.history && active.history.length > 0), capabilities: _vendorCaps, mode: active.mode || "gui", terminalId: typeof active.terminalId === "number" ? active.terminalId : null, runtimeMode: active.runtimeMode || null, runtimeTerminalId: typeof active.runtimeTerminalId === "number" ? active.runtimeTerminalId : null, tuiSuspended: !!active.tuiSuspended });
|
|
266
|
+
sendTo(ws, { type: "session_switched", id: active.localId, cliSessionId: active.cliSessionId || null, loop: active.loop || null, vendor: active.vendor || null, model: active.model || null, effort: active.effort || initialEffort || null, hasHistory: (active.history && active.history.length > 0), capabilities: _vendorCaps, mode: active.mode || "gui", terminalId: typeof active.terminalId === "number" ? active.terminalId : null, runtimeMode: active.runtimeMode || null, runtimeTerminalId: typeof active.runtimeTerminalId === "number" ? active.runtimeTerminalId : null, tuiSuspended: !!active.tuiSuspended, permissionMode: active.permissionMode || null });
|
|
267
|
+
if (ctx.autonomousRun) sendTo(ws, { type: "autonomous_run_state", sessionId: active.localId, run: ctx.autonomousRun.project(active.autonomousRun) });
|
|
266
268
|
// Send per-session context sources
|
|
267
269
|
var sessionSources = loadContextSources(slug, active.localId);
|
|
268
270
|
sendTo(ws, { type: "context_sources_state", active: sessionSources });
|
|
@@ -288,6 +290,7 @@ function attachConnection(ctx) {
|
|
|
288
290
|
}
|
|
289
291
|
}
|
|
290
292
|
sendTo(ws, { type: "history_done", lastUsage: _lastUsage, lastModelUsage: _lastModelUsage, lastCost: _lastCost, lastStreamInputTokens: _lastStreamInputTokens, contextUsage: active.lastContextUsage || null });
|
|
293
|
+
if (scheduledMessages) scheduledMessages.sendState(active, ws);
|
|
291
294
|
|
|
292
295
|
if (active.isProcessing) {
|
|
293
296
|
sendTo(ws, { type: "status", status: "processing" });
|