@reconcrap/boss-recommend-mcp 2.0.53 → 2.0.55
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/bin/boss-recommend-mcp.js +0 -0
- package/config/screening-config.example.json +1 -1
- package/package.json +120 -120
- package/src/cli.js +3121 -3121
- package/src/core/run/index.js +310 -310
- package/src/domains/chat/constants.js +229 -220
- package/src/domains/chat/detail.js +1686 -1653
- package/src/domains/chat/run-service.js +2039 -1979
- package/src/domains/common/account-rights-panel.js +314 -0
- package/src/domains/recommend/detail.js +546 -531
- package/src/domains/recommend/run-service.js +1245 -1180
- package/src/domains/recruit/detail.js +15 -0
- package/src/domains/recruit/run-service.js +78 -1
- package/src/recommend-mcp.js +1701 -1701
- package/src/run-state.js +358 -358
package/src/core/run/index.js
CHANGED
|
@@ -1,310 +1,310 @@
|
|
|
1
|
-
export const RUN_STATUS_QUEUED = "queued";
|
|
2
|
-
export const RUN_STATUS_RUNNING = "running";
|
|
3
|
-
export const RUN_STATUS_PAUSED = "paused";
|
|
4
|
-
export const RUN_STATUS_COMPLETED = "completed";
|
|
5
|
-
export const RUN_STATUS_CANCELING = "canceling";
|
|
6
|
-
export const RUN_STATUS_CANCELED = "canceled";
|
|
7
|
-
export const RUN_STATUS_FAILED = "failed";
|
|
8
|
-
|
|
9
|
-
const TERMINAL_STATUSES = new Set([
|
|
10
|
-
RUN_STATUS_COMPLETED,
|
|
11
|
-
RUN_STATUS_CANCELED,
|
|
12
|
-
RUN_STATUS_FAILED
|
|
13
|
-
]);
|
|
14
|
-
|
|
15
|
-
export class RunCanceledError extends Error {
|
|
16
|
-
constructor(message = "Run canceled") {
|
|
17
|
-
super(message);
|
|
18
|
-
this.name = "RunCanceledError";
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function nowIso() {
|
|
23
|
-
return new Date().toISOString();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function createRunId(prefix = "run") {
|
|
27
|
-
const random = Math.random().toString(36).slice(2, 10);
|
|
28
|
-
return `${prefix}_${Date.now().toString(36)}_${random}`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function clone(value) {
|
|
32
|
-
return JSON.parse(JSON.stringify(value));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function createDeferred() {
|
|
36
|
-
let resolve;
|
|
37
|
-
let reject;
|
|
38
|
-
const promise = new Promise((promiseResolve, promiseReject) => {
|
|
39
|
-
resolve = promiseResolve;
|
|
40
|
-
reject = promiseReject;
|
|
41
|
-
});
|
|
42
|
-
return { promise, resolve, reject };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function snapshotFromEntry(entry) {
|
|
46
|
-
const run = entry.run;
|
|
47
|
-
return clone({
|
|
48
|
-
runId: run.runId,
|
|
49
|
-
name: run.name,
|
|
50
|
-
pid: run.pid,
|
|
51
|
-
status: run.status,
|
|
52
|
-
phase: run.phase,
|
|
53
|
-
progress: run.progress,
|
|
54
|
-
context: run.context,
|
|
55
|
-
checkpoint: run.checkpoint,
|
|
56
|
-
startedAt: run.startedAt,
|
|
57
|
-
updatedAt: run.updatedAt,
|
|
58
|
-
completedAt: run.completedAt,
|
|
59
|
-
canPause: run.status === RUN_STATUS_RUNNING,
|
|
60
|
-
canResume: run.status === RUN_STATUS_PAUSED,
|
|
61
|
-
canCancel: !TERMINAL_STATUSES.has(run.status),
|
|
62
|
-
error: run.error,
|
|
63
|
-
summary: run.summary
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function createRunLifecycleManager({
|
|
68
|
-
idPrefix = "run",
|
|
69
|
-
now = nowIso,
|
|
70
|
-
onSnapshot = null
|
|
71
|
-
} = {}) {
|
|
72
|
-
const runs = new Map();
|
|
73
|
-
|
|
74
|
-
function emitSnapshot(entry, event = {}) {
|
|
75
|
-
if (typeof onSnapshot !== "function") return;
|
|
76
|
-
try {
|
|
77
|
-
onSnapshot(snapshotFromEntry(entry), {
|
|
78
|
-
type: event.type || "update",
|
|
79
|
-
at: now(),
|
|
80
|
-
...event
|
|
81
|
-
});
|
|
82
|
-
} catch {
|
|
83
|
-
// Snapshot hooks must never interrupt an active browser run.
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function getEntry(runId) {
|
|
88
|
-
const entry = runs.get(runId);
|
|
89
|
-
if (!entry) throw new Error(`Unknown runId: ${runId}`);
|
|
90
|
-
return entry;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function touch(entry) {
|
|
94
|
-
entry.run.updatedAt = now();
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function setStatus(entry, status, patch = {}) {
|
|
98
|
-
entry.run.status = status;
|
|
99
|
-
Object.assign(entry.run, patch);
|
|
100
|
-
touch(entry);
|
|
101
|
-
emitSnapshot(entry, { type: "status", status });
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function createControls(entry) {
|
|
105
|
-
return {
|
|
106
|
-
signal: entry.controller.signal,
|
|
107
|
-
get runId() {
|
|
108
|
-
return entry.run.runId;
|
|
109
|
-
},
|
|
110
|
-
get status() {
|
|
111
|
-
return entry.run.status;
|
|
112
|
-
},
|
|
113
|
-
setPhase(phase) {
|
|
114
|
-
entry.run.phase = phase;
|
|
115
|
-
touch(entry);
|
|
116
|
-
},
|
|
117
|
-
updateProgress(progressPatch = {}) {
|
|
118
|
-
entry.run.progress = {
|
|
119
|
-
...entry.run.progress,
|
|
120
|
-
...progressPatch
|
|
121
|
-
};
|
|
122
|
-
touch(entry);
|
|
123
|
-
emitSnapshot(entry, { type: "progress", progressPatch });
|
|
124
|
-
return snapshotFromEntry(entry);
|
|
125
|
-
},
|
|
126
|
-
checkpoint(checkpointPatch = {}) {
|
|
127
|
-
entry.run.checkpoint = {
|
|
128
|
-
...entry.run.checkpoint,
|
|
129
|
-
...checkpointPatch,
|
|
130
|
-
updatedAt: now()
|
|
131
|
-
};
|
|
132
|
-
touch(entry);
|
|
133
|
-
emitSnapshot(entry, { type: "checkpoint", checkpointPatch });
|
|
134
|
-
return snapshotFromEntry(entry);
|
|
135
|
-
},
|
|
136
|
-
async waitIfPaused() {
|
|
137
|
-
if (entry.controller.signal.aborted) {
|
|
138
|
-
throw new RunCanceledError();
|
|
139
|
-
}
|
|
140
|
-
if (!entry.pauseRequested) return;
|
|
141
|
-
setStatus(entry, RUN_STATUS_PAUSED);
|
|
142
|
-
while (entry.pauseRequested) {
|
|
143
|
-
const deferred = createDeferred();
|
|
144
|
-
entry.pauseWaiters.add(deferred);
|
|
145
|
-
try {
|
|
146
|
-
await deferred.promise;
|
|
147
|
-
} finally {
|
|
148
|
-
entry.pauseWaiters.delete(deferred);
|
|
149
|
-
}
|
|
150
|
-
if (entry.controller.signal.aborted) {
|
|
151
|
-
throw new RunCanceledError();
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
setStatus(entry, RUN_STATUS_RUNNING);
|
|
155
|
-
},
|
|
156
|
-
async sleep(ms) {
|
|
157
|
-
if (entry.controller.signal.aborted) {
|
|
158
|
-
throw new RunCanceledError();
|
|
159
|
-
}
|
|
160
|
-
await new Promise((resolve, reject) => {
|
|
161
|
-
const timer = setTimeout(resolve, ms);
|
|
162
|
-
const onAbort = () => {
|
|
163
|
-
clearTimeout(timer);
|
|
164
|
-
reject(new RunCanceledError());
|
|
165
|
-
};
|
|
166
|
-
entry.controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
167
|
-
});
|
|
168
|
-
},
|
|
169
|
-
throwIfCanceled() {
|
|
170
|
-
if (entry.controller.signal.aborted) {
|
|
171
|
-
throw new RunCanceledError();
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async function settle(entry, task) {
|
|
178
|
-
try {
|
|
179
|
-
const summary = await task(entry.controls);
|
|
180
|
-
if (entry.controller.signal.aborted || entry.cancelRequested) {
|
|
181
|
-
setStatus(entry, RUN_STATUS_CANCELED, {
|
|
182
|
-
completedAt: now(),
|
|
183
|
-
summary: summary || entry.run.summary
|
|
184
|
-
});
|
|
185
|
-
} else {
|
|
186
|
-
setStatus(entry, RUN_STATUS_COMPLETED, {
|
|
187
|
-
completedAt: now(),
|
|
188
|
-
summary: summary || entry.run.summary
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
} catch (error) {
|
|
192
|
-
if (error instanceof RunCanceledError || entry.controller.signal.aborted || entry.cancelRequested) {
|
|
193
|
-
setStatus(entry, RUN_STATUS_CANCELED, {
|
|
194
|
-
completedAt: now(),
|
|
195
|
-
error: null
|
|
196
|
-
});
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
setStatus(entry, RUN_STATUS_FAILED, {
|
|
200
|
-
completedAt: now(),
|
|
201
|
-
error: {
|
|
202
|
-
name: error?.name || "Error",
|
|
203
|
-
message: error?.message || String(error)
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function startRun({ runId: requestedRunId = "", name, pid = process.pid, context = {}, progress = {}, checkpoint = {}, task }) {
|
|
210
|
-
if (typeof task !== "function") {
|
|
211
|
-
throw new Error("startRun requires a task function");
|
|
212
|
-
}
|
|
213
|
-
const runId = String(requestedRunId || "").trim() || createRunId(idPrefix);
|
|
214
|
-
if (runs.has(runId)) {
|
|
215
|
-
throw new Error(`Run already exists: ${runId}`);
|
|
216
|
-
}
|
|
217
|
-
const startedAt = now();
|
|
218
|
-
const entry = {
|
|
219
|
-
controller: new AbortController(),
|
|
220
|
-
pauseRequested: false,
|
|
221
|
-
cancelRequested: false,
|
|
222
|
-
pauseWaiters: new Set(),
|
|
223
|
-
run: {
|
|
224
|
-
runId,
|
|
225
|
-
name: name || runId,
|
|
226
|
-
pid: Number.isInteger(pid) && pid > 0 ? pid : process.pid,
|
|
227
|
-
status: RUN_STATUS_QUEUED,
|
|
228
|
-
phase: "queued",
|
|
229
|
-
progress,
|
|
230
|
-
context,
|
|
231
|
-
checkpoint,
|
|
232
|
-
startedAt,
|
|
233
|
-
updatedAt: startedAt,
|
|
234
|
-
completedAt: null,
|
|
235
|
-
error: null,
|
|
236
|
-
summary: null
|
|
237
|
-
}
|
|
238
|
-
};
|
|
239
|
-
entry.controls = createControls(entry);
|
|
240
|
-
runs.set(runId, entry);
|
|
241
|
-
setStatus(entry, RUN_STATUS_RUNNING, { phase: "running" });
|
|
242
|
-
entry.promise = settle(entry, task);
|
|
243
|
-
return snapshotFromEntry(entry);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function getRun(runId) {
|
|
247
|
-
return snapshotFromEntry(getEntry(runId));
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function pauseRun(runId) {
|
|
251
|
-
const entry = getEntry(runId);
|
|
252
|
-
if (TERMINAL_STATUSES.has(entry.run.status)) return snapshotFromEntry(entry);
|
|
253
|
-
entry.pauseRequested = true;
|
|
254
|
-
if (entry.run.status === RUN_STATUS_RUNNING) {
|
|
255
|
-
touch(entry);
|
|
256
|
-
emitSnapshot(entry, { type: "pause_requested" });
|
|
257
|
-
}
|
|
258
|
-
return snapshotFromEntry(entry);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function resumeRun(runId) {
|
|
262
|
-
const entry = getEntry(runId);
|
|
263
|
-
if (TERMINAL_STATUSES.has(entry.run.status)) return snapshotFromEntry(entry);
|
|
264
|
-
entry.pauseRequested = false;
|
|
265
|
-
for (const waiter of entry.pauseWaiters) {
|
|
266
|
-
waiter.resolve();
|
|
267
|
-
}
|
|
268
|
-
if (entry.run.status === RUN_STATUS_PAUSED) {
|
|
269
|
-
setStatus(entry, RUN_STATUS_RUNNING);
|
|
270
|
-
} else {
|
|
271
|
-
touch(entry);
|
|
272
|
-
emitSnapshot(entry, { type: "resume_requested" });
|
|
273
|
-
}
|
|
274
|
-
return snapshotFromEntry(entry);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function cancelRun(runId) {
|
|
278
|
-
const entry = getEntry(runId);
|
|
279
|
-
if (TERMINAL_STATUSES.has(entry.run.status)) return snapshotFromEntry(entry);
|
|
280
|
-
entry.cancelRequested = true;
|
|
281
|
-
setStatus(entry, RUN_STATUS_CANCELING);
|
|
282
|
-
entry.controller.abort();
|
|
283
|
-
entry.pauseRequested = false;
|
|
284
|
-
for (const waiter of entry.pauseWaiters) {
|
|
285
|
-
waiter.resolve();
|
|
286
|
-
}
|
|
287
|
-
return snapshotFromEntry(entry);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
async function waitForRun(runId, { timeoutMs = 10000 } = {}) {
|
|
291
|
-
const entry = getEntry(runId);
|
|
292
|
-
const timeout = new Promise((_, reject) => {
|
|
293
|
-
setTimeout(() => reject(new Error(`Timed out waiting for run ${runId}`)), timeoutMs);
|
|
294
|
-
});
|
|
295
|
-
await Promise.race([entry.promise, timeout]);
|
|
296
|
-
return snapshotFromEntry(entry);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return {
|
|
300
|
-
startRun,
|
|
301
|
-
getRun,
|
|
302
|
-
pauseRun,
|
|
303
|
-
resumeRun,
|
|
304
|
-
cancelRun,
|
|
305
|
-
waitForRun,
|
|
306
|
-
listRuns() {
|
|
307
|
-
return Array.from(runs.values()).map(snapshotFromEntry);
|
|
308
|
-
}
|
|
309
|
-
};
|
|
310
|
-
}
|
|
1
|
+
export const RUN_STATUS_QUEUED = "queued";
|
|
2
|
+
export const RUN_STATUS_RUNNING = "running";
|
|
3
|
+
export const RUN_STATUS_PAUSED = "paused";
|
|
4
|
+
export const RUN_STATUS_COMPLETED = "completed";
|
|
5
|
+
export const RUN_STATUS_CANCELING = "canceling";
|
|
6
|
+
export const RUN_STATUS_CANCELED = "canceled";
|
|
7
|
+
export const RUN_STATUS_FAILED = "failed";
|
|
8
|
+
|
|
9
|
+
const TERMINAL_STATUSES = new Set([
|
|
10
|
+
RUN_STATUS_COMPLETED,
|
|
11
|
+
RUN_STATUS_CANCELED,
|
|
12
|
+
RUN_STATUS_FAILED
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
export class RunCanceledError extends Error {
|
|
16
|
+
constructor(message = "Run canceled") {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "RunCanceledError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function nowIso() {
|
|
23
|
+
return new Date().toISOString();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createRunId(prefix = "run") {
|
|
27
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
28
|
+
return `${prefix}_${Date.now().toString(36)}_${random}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function clone(value) {
|
|
32
|
+
return JSON.parse(JSON.stringify(value));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createDeferred() {
|
|
36
|
+
let resolve;
|
|
37
|
+
let reject;
|
|
38
|
+
const promise = new Promise((promiseResolve, promiseReject) => {
|
|
39
|
+
resolve = promiseResolve;
|
|
40
|
+
reject = promiseReject;
|
|
41
|
+
});
|
|
42
|
+
return { promise, resolve, reject };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function snapshotFromEntry(entry) {
|
|
46
|
+
const run = entry.run;
|
|
47
|
+
return clone({
|
|
48
|
+
runId: run.runId,
|
|
49
|
+
name: run.name,
|
|
50
|
+
pid: run.pid,
|
|
51
|
+
status: run.status,
|
|
52
|
+
phase: run.phase,
|
|
53
|
+
progress: run.progress,
|
|
54
|
+
context: run.context,
|
|
55
|
+
checkpoint: run.checkpoint,
|
|
56
|
+
startedAt: run.startedAt,
|
|
57
|
+
updatedAt: run.updatedAt,
|
|
58
|
+
completedAt: run.completedAt,
|
|
59
|
+
canPause: run.status === RUN_STATUS_RUNNING,
|
|
60
|
+
canResume: run.status === RUN_STATUS_PAUSED,
|
|
61
|
+
canCancel: !TERMINAL_STATUSES.has(run.status),
|
|
62
|
+
error: run.error,
|
|
63
|
+
summary: run.summary
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createRunLifecycleManager({
|
|
68
|
+
idPrefix = "run",
|
|
69
|
+
now = nowIso,
|
|
70
|
+
onSnapshot = null
|
|
71
|
+
} = {}) {
|
|
72
|
+
const runs = new Map();
|
|
73
|
+
|
|
74
|
+
function emitSnapshot(entry, event = {}) {
|
|
75
|
+
if (typeof onSnapshot !== "function") return;
|
|
76
|
+
try {
|
|
77
|
+
onSnapshot(snapshotFromEntry(entry), {
|
|
78
|
+
type: event.type || "update",
|
|
79
|
+
at: now(),
|
|
80
|
+
...event
|
|
81
|
+
});
|
|
82
|
+
} catch {
|
|
83
|
+
// Snapshot hooks must never interrupt an active browser run.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getEntry(runId) {
|
|
88
|
+
const entry = runs.get(runId);
|
|
89
|
+
if (!entry) throw new Error(`Unknown runId: ${runId}`);
|
|
90
|
+
return entry;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function touch(entry) {
|
|
94
|
+
entry.run.updatedAt = now();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function setStatus(entry, status, patch = {}) {
|
|
98
|
+
entry.run.status = status;
|
|
99
|
+
Object.assign(entry.run, patch);
|
|
100
|
+
touch(entry);
|
|
101
|
+
emitSnapshot(entry, { type: "status", status });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function createControls(entry) {
|
|
105
|
+
return {
|
|
106
|
+
signal: entry.controller.signal,
|
|
107
|
+
get runId() {
|
|
108
|
+
return entry.run.runId;
|
|
109
|
+
},
|
|
110
|
+
get status() {
|
|
111
|
+
return entry.run.status;
|
|
112
|
+
},
|
|
113
|
+
setPhase(phase) {
|
|
114
|
+
entry.run.phase = phase;
|
|
115
|
+
touch(entry);
|
|
116
|
+
},
|
|
117
|
+
updateProgress(progressPatch = {}) {
|
|
118
|
+
entry.run.progress = {
|
|
119
|
+
...entry.run.progress,
|
|
120
|
+
...progressPatch
|
|
121
|
+
};
|
|
122
|
+
touch(entry);
|
|
123
|
+
emitSnapshot(entry, { type: "progress", progressPatch });
|
|
124
|
+
return snapshotFromEntry(entry);
|
|
125
|
+
},
|
|
126
|
+
checkpoint(checkpointPatch = {}) {
|
|
127
|
+
entry.run.checkpoint = {
|
|
128
|
+
...entry.run.checkpoint,
|
|
129
|
+
...checkpointPatch,
|
|
130
|
+
updatedAt: now()
|
|
131
|
+
};
|
|
132
|
+
touch(entry);
|
|
133
|
+
emitSnapshot(entry, { type: "checkpoint", checkpointPatch });
|
|
134
|
+
return snapshotFromEntry(entry);
|
|
135
|
+
},
|
|
136
|
+
async waitIfPaused() {
|
|
137
|
+
if (entry.controller.signal.aborted) {
|
|
138
|
+
throw new RunCanceledError();
|
|
139
|
+
}
|
|
140
|
+
if (!entry.pauseRequested) return;
|
|
141
|
+
setStatus(entry, RUN_STATUS_PAUSED);
|
|
142
|
+
while (entry.pauseRequested) {
|
|
143
|
+
const deferred = createDeferred();
|
|
144
|
+
entry.pauseWaiters.add(deferred);
|
|
145
|
+
try {
|
|
146
|
+
await deferred.promise;
|
|
147
|
+
} finally {
|
|
148
|
+
entry.pauseWaiters.delete(deferred);
|
|
149
|
+
}
|
|
150
|
+
if (entry.controller.signal.aborted) {
|
|
151
|
+
throw new RunCanceledError();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
setStatus(entry, RUN_STATUS_RUNNING);
|
|
155
|
+
},
|
|
156
|
+
async sleep(ms) {
|
|
157
|
+
if (entry.controller.signal.aborted) {
|
|
158
|
+
throw new RunCanceledError();
|
|
159
|
+
}
|
|
160
|
+
await new Promise((resolve, reject) => {
|
|
161
|
+
const timer = setTimeout(resolve, ms);
|
|
162
|
+
const onAbort = () => {
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
reject(new RunCanceledError());
|
|
165
|
+
};
|
|
166
|
+
entry.controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
167
|
+
});
|
|
168
|
+
},
|
|
169
|
+
throwIfCanceled() {
|
|
170
|
+
if (entry.controller.signal.aborted) {
|
|
171
|
+
throw new RunCanceledError();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function settle(entry, task) {
|
|
178
|
+
try {
|
|
179
|
+
const summary = await task(entry.controls);
|
|
180
|
+
if (entry.controller.signal.aborted || entry.cancelRequested) {
|
|
181
|
+
setStatus(entry, RUN_STATUS_CANCELED, {
|
|
182
|
+
completedAt: now(),
|
|
183
|
+
summary: summary || entry.run.summary
|
|
184
|
+
});
|
|
185
|
+
} else {
|
|
186
|
+
setStatus(entry, RUN_STATUS_COMPLETED, {
|
|
187
|
+
completedAt: now(),
|
|
188
|
+
summary: summary || entry.run.summary
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (error instanceof RunCanceledError || entry.controller.signal.aborted || entry.cancelRequested) {
|
|
193
|
+
setStatus(entry, RUN_STATUS_CANCELED, {
|
|
194
|
+
completedAt: now(),
|
|
195
|
+
error: null
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
setStatus(entry, RUN_STATUS_FAILED, {
|
|
200
|
+
completedAt: now(),
|
|
201
|
+
error: {
|
|
202
|
+
name: error?.name || "Error",
|
|
203
|
+
message: error?.message || String(error)
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function startRun({ runId: requestedRunId = "", name, pid = process.pid, context = {}, progress = {}, checkpoint = {}, task }) {
|
|
210
|
+
if (typeof task !== "function") {
|
|
211
|
+
throw new Error("startRun requires a task function");
|
|
212
|
+
}
|
|
213
|
+
const runId = String(requestedRunId || "").trim() || createRunId(idPrefix);
|
|
214
|
+
if (runs.has(runId)) {
|
|
215
|
+
throw new Error(`Run already exists: ${runId}`);
|
|
216
|
+
}
|
|
217
|
+
const startedAt = now();
|
|
218
|
+
const entry = {
|
|
219
|
+
controller: new AbortController(),
|
|
220
|
+
pauseRequested: false,
|
|
221
|
+
cancelRequested: false,
|
|
222
|
+
pauseWaiters: new Set(),
|
|
223
|
+
run: {
|
|
224
|
+
runId,
|
|
225
|
+
name: name || runId,
|
|
226
|
+
pid: Number.isInteger(pid) && pid > 0 ? pid : process.pid,
|
|
227
|
+
status: RUN_STATUS_QUEUED,
|
|
228
|
+
phase: "queued",
|
|
229
|
+
progress,
|
|
230
|
+
context,
|
|
231
|
+
checkpoint,
|
|
232
|
+
startedAt,
|
|
233
|
+
updatedAt: startedAt,
|
|
234
|
+
completedAt: null,
|
|
235
|
+
error: null,
|
|
236
|
+
summary: null
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
entry.controls = createControls(entry);
|
|
240
|
+
runs.set(runId, entry);
|
|
241
|
+
setStatus(entry, RUN_STATUS_RUNNING, { phase: "running" });
|
|
242
|
+
entry.promise = settle(entry, task);
|
|
243
|
+
return snapshotFromEntry(entry);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function getRun(runId) {
|
|
247
|
+
return snapshotFromEntry(getEntry(runId));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function pauseRun(runId) {
|
|
251
|
+
const entry = getEntry(runId);
|
|
252
|
+
if (TERMINAL_STATUSES.has(entry.run.status)) return snapshotFromEntry(entry);
|
|
253
|
+
entry.pauseRequested = true;
|
|
254
|
+
if (entry.run.status === RUN_STATUS_RUNNING) {
|
|
255
|
+
touch(entry);
|
|
256
|
+
emitSnapshot(entry, { type: "pause_requested" });
|
|
257
|
+
}
|
|
258
|
+
return snapshotFromEntry(entry);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function resumeRun(runId) {
|
|
262
|
+
const entry = getEntry(runId);
|
|
263
|
+
if (TERMINAL_STATUSES.has(entry.run.status)) return snapshotFromEntry(entry);
|
|
264
|
+
entry.pauseRequested = false;
|
|
265
|
+
for (const waiter of entry.pauseWaiters) {
|
|
266
|
+
waiter.resolve();
|
|
267
|
+
}
|
|
268
|
+
if (entry.run.status === RUN_STATUS_PAUSED) {
|
|
269
|
+
setStatus(entry, RUN_STATUS_RUNNING);
|
|
270
|
+
} else {
|
|
271
|
+
touch(entry);
|
|
272
|
+
emitSnapshot(entry, { type: "resume_requested" });
|
|
273
|
+
}
|
|
274
|
+
return snapshotFromEntry(entry);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function cancelRun(runId) {
|
|
278
|
+
const entry = getEntry(runId);
|
|
279
|
+
if (TERMINAL_STATUSES.has(entry.run.status)) return snapshotFromEntry(entry);
|
|
280
|
+
entry.cancelRequested = true;
|
|
281
|
+
setStatus(entry, RUN_STATUS_CANCELING);
|
|
282
|
+
entry.controller.abort();
|
|
283
|
+
entry.pauseRequested = false;
|
|
284
|
+
for (const waiter of entry.pauseWaiters) {
|
|
285
|
+
waiter.resolve();
|
|
286
|
+
}
|
|
287
|
+
return snapshotFromEntry(entry);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function waitForRun(runId, { timeoutMs = 10000 } = {}) {
|
|
291
|
+
const entry = getEntry(runId);
|
|
292
|
+
const timeout = new Promise((_, reject) => {
|
|
293
|
+
setTimeout(() => reject(new Error(`Timed out waiting for run ${runId}`)), timeoutMs);
|
|
294
|
+
});
|
|
295
|
+
await Promise.race([entry.promise, timeout]);
|
|
296
|
+
return snapshotFromEntry(entry);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
startRun,
|
|
301
|
+
getRun,
|
|
302
|
+
pauseRun,
|
|
303
|
+
resumeRun,
|
|
304
|
+
cancelRun,
|
|
305
|
+
waitForRun,
|
|
306
|
+
listRuns() {
|
|
307
|
+
return Array.from(runs.values()).map(snapshotFromEntry);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|