@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.23
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/README.md +5 -2
- package/bin/boss-recommend-mcp.js +4 -4
- package/config/screening-config.example.json +33 -33
- package/package.json +2 -1
- package/scripts/install-macos.sh +280 -280
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-chat/README.md +42 -42
- package/skills/boss-chat/SKILL.md +106 -106
- package/skills/boss-recommend-pipeline/README.md +13 -13
- package/skills/boss-recommend-pipeline/SKILL.md +219 -214
- package/skills/boss-recruit-pipeline/README.md +19 -19
- package/skills/boss-recruit-pipeline/SKILL.md +89 -89
- package/src/chat-mcp.js +127 -127
- package/src/chat-runtime-config.js +775 -775
- package/src/cli.js +573 -573
- package/src/core/boss-cards/index.js +199 -199
- package/src/core/browser/index.js +2416 -2385
- package/src/core/capture/index.js +1201 -1201
- package/src/core/cv-acquisition/index.js +238 -238
- package/src/core/cv-capture-target/index.js +299 -299
- package/src/core/greet-quota/index.js +71 -71
- package/src/core/infinite-list/index.js +1315 -1306
- package/src/core/reporting/legacy-csv.js +334 -332
- package/src/core/run/index.js +44 -27
- package/src/core/run/timing.js +33 -33
- package/src/core/screening/index.js +2135 -2135
- package/src/core/self-heal/index.js +973 -973
- package/src/core/self-heal/viewport.js +564 -564
- package/src/detached-worker.js +99 -99
- package/src/domains/chat/cards.js +137 -137
- package/src/domains/chat/constants.js +9 -9
- package/src/domains/chat/detail.js +113 -113
- package/src/domains/chat/index.js +7 -7
- package/src/domains/chat/jobs.js +620 -620
- package/src/domains/chat/page-guard.js +122 -122
- package/src/domains/chat/roots.js +56 -56
- package/src/domains/chat/run-service.js +571 -571
- package/src/domains/common/account-rights-panel.js +314 -314
- package/src/domains/common/recovery-settle.js +159 -159
- package/src/domains/recommend/actions.js +472 -472
- package/src/domains/recommend/cards.js +243 -243
- package/src/domains/recommend/colleague-contact.js +333 -333
- package/src/domains/recommend/constants.js +228 -159
- package/src/domains/recommend/detail.js +655 -646
- package/src/domains/recommend/filters.js +748 -377
- package/src/domains/recommend/index.js +4 -3
- package/src/domains/recommend/jobs.js +542 -542
- package/src/domains/recommend/location.js +736 -0
- package/src/domains/recommend/refresh.js +575 -351
- package/src/domains/recommend/roots.js +80 -80
- package/src/domains/recommend/run-service.js +1616 -878
- package/src/domains/recommend/scopes.js +246 -246
- package/src/domains/recruit/actions.js +277 -277
- package/src/domains/recruit/cards.js +74 -74
- package/src/domains/recruit/constants.js +236 -236
- package/src/domains/recruit/detail.js +588 -588
- package/src/domains/recruit/index.js +9 -9
- package/src/domains/recruit/instruction-parser.js +866 -866
- package/src/domains/recruit/refresh.js +45 -45
- package/src/domains/recruit/roots.js +68 -68
- package/src/domains/recruit/run-service.js +1620 -1620
- package/src/domains/recruit/search.js +3229 -3229
- package/src/index.js +121 -4
- package/src/parser.js +376 -8
- package/src/recommend-mcp.js +1174 -914
- package/src/recommend-scheduler.js +535 -469
- package/src/recruit-mcp.js +2121 -2121
|
@@ -1,496 +1,562 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import process from "node:process";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { getStateHome } from "./run-state.js";
|
|
7
|
-
import {
|
|
8
|
-
compactRecommendRunForStatus,
|
|
9
|
-
getRecommendPipelineRunTool,
|
|
10
|
-
prepareRecommendPipelineRunTool,
|
|
11
|
-
startRecommendPipelineRunTool
|
|
12
|
-
} from "./recommend-mcp.js";
|
|
13
|
-
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { getStateHome } from "./run-state.js";
|
|
7
|
+
import {
|
|
8
|
+
compactRecommendRunForStatus,
|
|
9
|
+
getRecommendPipelineRunTool,
|
|
10
|
+
prepareRecommendPipelineRunTool,
|
|
11
|
+
startRecommendPipelineRunTool
|
|
12
|
+
} from "./recommend-mcp.js";
|
|
13
|
+
|
|
14
14
|
const SCHEDULE_WORKER_FLAG = "--schedule-worker";
|
|
15
15
|
const SCHEDULE_ID_FLAG = "--schedule-id";
|
|
16
16
|
const TERMINAL_SCHEDULE_STATES = new Set(["completed", "failed", "canceled"]);
|
|
17
17
|
const TERMINAL_RUN_STATES = new Set(["completed", "failed", "canceled"]);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
const SCHEDULE_FORBIDDEN_DEBUG_FIELDS = Object.freeze([
|
|
19
|
+
"debug_test_mode",
|
|
20
|
+
"allow_debug_test_mode",
|
|
21
|
+
"debug_force_list_end_after_processed",
|
|
22
|
+
"debug_force_context_recovery_after_processed",
|
|
23
|
+
"debug_force_cdp_reconnect_after_processed"
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
let spawnProcessImpl = spawn;
|
|
27
|
+
|
|
28
|
+
function normalizeText(value) {
|
|
29
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
function clonePlain(value, fallback = null) {
|
|
26
|
-
try {
|
|
27
|
-
return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
|
|
28
|
-
} catch {
|
|
29
|
-
return fallback;
|
|
30
|
-
}
|
|
33
|
+
try {
|
|
34
|
+
return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
|
|
35
|
+
} catch {
|
|
36
|
+
return fallback;
|
|
37
|
+
}
|
|
31
38
|
}
|
|
32
39
|
|
|
33
|
-
function
|
|
34
|
-
return
|
|
40
|
+
function hasOwn(source, key) {
|
|
41
|
+
return Boolean(source && Object.prototype.hasOwnProperty.call(source, key));
|
|
35
42
|
}
|
|
36
43
|
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
function parseNonNegativeNumber(raw, fallback = null) {
|
|
42
|
-
if (raw === undefined || raw === null || raw === "") return fallback;
|
|
43
|
-
const parsed = Number(raw);
|
|
44
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function parseRunAt(args = {}) {
|
|
48
|
-
const direct = normalizeText(args.schedule_run_at || args.scheduleRunAt || args.run_at || args.runAt);
|
|
49
|
-
if (direct) {
|
|
50
|
-
const timestamp = Date.parse(direct);
|
|
51
|
-
if (!Number.isFinite(timestamp)) {
|
|
52
|
-
return {
|
|
53
|
-
ok: false,
|
|
54
|
-
error: `Invalid schedule_run_at: ${direct}`
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
return {
|
|
58
|
-
ok: true,
|
|
59
|
-
runAtMs: timestamp,
|
|
60
|
-
source: "schedule_run_at"
|
|
61
|
-
};
|
|
44
|
+
function collectScheduledRecommendDebugOptions(args = {}) {
|
|
45
|
+
const options = [];
|
|
46
|
+
for (const field of SCHEDULE_FORBIDDEN_DEBUG_FIELDS) {
|
|
47
|
+
if (hasOwn(args, field)) options.push(field);
|
|
62
48
|
}
|
|
63
49
|
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
ok: true,
|
|
68
|
-
runAtMs: Date.now() + Math.round(delaySeconds * 1000),
|
|
69
|
-
source: "schedule_delay_seconds"
|
|
70
|
-
};
|
|
50
|
+
const screeningMode = normalizeText(args.screening_mode || args.screeningMode).toLowerCase();
|
|
51
|
+
if (["deterministic", "local", "local_scorer"].includes(screeningMode)) {
|
|
52
|
+
options.push(`screening_mode=${screeningMode}`);
|
|
71
53
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (delayMinutes !== null) {
|
|
75
|
-
return {
|
|
76
|
-
ok: true,
|
|
77
|
-
runAtMs: Date.now() + Math.round(delayMinutes * 60 * 1000),
|
|
78
|
-
source: "schedule_delay_minutes"
|
|
79
|
-
};
|
|
54
|
+
if (args.use_llm === false || args.useLlm === false) {
|
|
55
|
+
options.push("use_llm=false");
|
|
80
56
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
ok: false,
|
|
84
|
-
error: "schedule_run_at or schedule_delay_minutes/schedule_delay_seconds is required"
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function safeIdPart(value) {
|
|
89
|
-
return normalizeText(value).replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function createScheduleId(raw = "") {
|
|
93
|
-
const requested = safeIdPart(raw);
|
|
94
|
-
if (requested) return requested;
|
|
95
|
-
const suffix = Math.random().toString(36).slice(2, 10);
|
|
96
|
-
return `mcp_recommend_schedule_${Date.now().toString(36)}_${suffix}`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function getSchedulesDir() {
|
|
100
|
-
return path.join(getStateHome(), "schedules");
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function getScheduleArtifacts(scheduleId) {
|
|
104
|
-
const id = safeIdPart(scheduleId);
|
|
105
|
-
if (!id) throw new Error("schedule_id is required");
|
|
106
|
-
return {
|
|
107
|
-
schedule_path: path.join(getSchedulesDir(), `${id}.json`),
|
|
108
|
-
worker_stdout_path: path.join(getSchedulesDir(), `${id}.worker.stdout.log`),
|
|
109
|
-
worker_stderr_path: path.join(getSchedulesDir(), `${id}.worker.stderr.log`)
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function writeJsonAtomic(filePath, payload) {
|
|
114
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
115
|
-
const tempPath = `${filePath}.tmp`;
|
|
116
|
-
fs.writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
117
|
-
fs.renameSync(tempPath, filePath);
|
|
118
|
-
return payload;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function readJsonFile(filePath) {
|
|
122
|
-
try {
|
|
123
|
-
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
124
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
125
|
-
} catch {
|
|
126
|
-
return null;
|
|
57
|
+
if (args.allow_card_only_screening === true || args.allowCardOnlyScreening === true) {
|
|
58
|
+
options.push("allow_card_only_screening");
|
|
127
59
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
function writeSchedule(scheduleId, patch) {
|
|
136
|
-
const artifacts = getScheduleArtifacts(scheduleId);
|
|
137
|
-
const current = readJsonFile(artifacts.schedule_path) || {};
|
|
138
|
-
return writeJsonAtomic(artifacts.schedule_path, {
|
|
139
|
-
...current,
|
|
140
|
-
...patch,
|
|
141
|
-
schedule_id: scheduleId,
|
|
142
|
-
updated_at: nowIso()
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function compactScheduleForStatus(schedule) {
|
|
147
|
-
if (!schedule || typeof schedule !== "object" || Array.isArray(schedule)) return schedule || null;
|
|
148
|
-
const compact = clonePlain(schedule, schedule);
|
|
149
|
-
if (compact.run) compact.run = compactRecommendRunForStatus(compact.run);
|
|
150
|
-
if (compact.launch_payload?.run) {
|
|
151
|
-
compact.launch_payload = {
|
|
152
|
-
...compact.launch_payload,
|
|
153
|
-
run: compactRecommendRunForStatus(compact.launch_payload.run)
|
|
154
|
-
};
|
|
60
|
+
const detailLimit = Number.parseInt(String(args.detail_limit ?? args.detailLimit ?? ""), 10);
|
|
61
|
+
if (Number.isFinite(detailLimit) && detailLimit === 0) options.push("detail_limit=0");
|
|
62
|
+
if (args.no_filter === true || args.noFilter === true) options.push("no_filter");
|
|
63
|
+
if (args.filter_enabled === false || args.filterEnabled === false) options.push("filter_enabled=false");
|
|
64
|
+
if (args.dry_run_post_action === true || args.dryRunPostAction === true) {
|
|
65
|
+
options.push("dry_run_post_action");
|
|
155
66
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
67
|
+
if (args.dry_run === true || args.dryRun === true) options.push("dry_run");
|
|
68
|
+
|
|
69
|
+
const requestedPostAction = normalizeText(
|
|
70
|
+
args.overrides?.post_action
|
|
71
|
+
?? args.confirmation?.post_action_value
|
|
72
|
+
?? args.post_action
|
|
73
|
+
?? args.postAction
|
|
74
|
+
).toLowerCase();
|
|
75
|
+
if (
|
|
76
|
+
requestedPostAction
|
|
77
|
+
&& requestedPostAction !== "none"
|
|
78
|
+
&& (args.execute_post_action === false || args.executePostAction === false)
|
|
79
|
+
) {
|
|
80
|
+
options.push("execute_post_action=false");
|
|
166
81
|
}
|
|
82
|
+
return Array.from(new Set(options));
|
|
167
83
|
}
|
|
168
|
-
|
|
169
|
-
function
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
return
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
84
|
+
|
|
85
|
+
function nowIso() {
|
|
86
|
+
return new Date().toISOString();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sleep(ms) {
|
|
90
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parseNonNegativeNumber(raw, fallback = null) {
|
|
94
|
+
if (raw === undefined || raw === null || raw === "") return fallback;
|
|
95
|
+
const parsed = Number(raw);
|
|
96
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseRunAt(args = {}) {
|
|
100
|
+
const direct = normalizeText(args.schedule_run_at || args.scheduleRunAt || args.run_at || args.runAt);
|
|
101
|
+
if (direct) {
|
|
102
|
+
const timestamp = Date.parse(direct);
|
|
103
|
+
if (!Number.isFinite(timestamp)) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
error: `Invalid schedule_run_at: ${direct}`
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
ok: true,
|
|
111
|
+
runAtMs: timestamp,
|
|
112
|
+
source: "schedule_run_at"
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const delaySeconds = parseNonNegativeNumber(args.schedule_delay_seconds ?? args.scheduleDelaySeconds, null);
|
|
117
|
+
if (delaySeconds !== null) {
|
|
118
|
+
return {
|
|
119
|
+
ok: true,
|
|
120
|
+
runAtMs: Date.now() + Math.round(delaySeconds * 1000),
|
|
121
|
+
source: "schedule_delay_seconds"
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const delayMinutes = parseNonNegativeNumber(args.schedule_delay_minutes ?? args.scheduleDelayMinutes, null);
|
|
126
|
+
if (delayMinutes !== null) {
|
|
127
|
+
return {
|
|
128
|
+
ok: true,
|
|
129
|
+
runAtMs: Date.now() + Math.round(delayMinutes * 60 * 1000),
|
|
130
|
+
source: "schedule_delay_minutes"
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
error: "schedule_run_at or schedule_delay_minutes/schedule_delay_seconds is required"
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function safeIdPart(value) {
|
|
141
|
+
return normalizeText(value).replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function createScheduleId(raw = "") {
|
|
145
|
+
const requested = safeIdPart(raw);
|
|
146
|
+
if (requested) return requested;
|
|
147
|
+
const suffix = Math.random().toString(36).slice(2, 10);
|
|
148
|
+
return `mcp_recommend_schedule_${Date.now().toString(36)}_${suffix}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getSchedulesDir() {
|
|
152
|
+
return path.join(getStateHome(), "schedules");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getScheduleArtifacts(scheduleId) {
|
|
156
|
+
const id = safeIdPart(scheduleId);
|
|
157
|
+
if (!id) throw new Error("schedule_id is required");
|
|
158
|
+
return {
|
|
159
|
+
schedule_path: path.join(getSchedulesDir(), `${id}.json`),
|
|
160
|
+
worker_stdout_path: path.join(getSchedulesDir(), `${id}.worker.stdout.log`),
|
|
161
|
+
worker_stderr_path: path.join(getSchedulesDir(), `${id}.worker.stderr.log`)
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function writeJsonAtomic(filePath, payload) {
|
|
166
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
167
|
+
const tempPath = `${filePath}.tmp`;
|
|
168
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
169
|
+
fs.renameSync(tempPath, filePath);
|
|
170
|
+
return payload;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function readJsonFile(filePath) {
|
|
174
|
+
try {
|
|
175
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
176
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
177
|
+
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function readSchedule(scheduleId) {
|
|
183
|
+
const artifacts = getScheduleArtifacts(scheduleId);
|
|
184
|
+
return readJsonFile(artifacts.schedule_path);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function writeSchedule(scheduleId, patch) {
|
|
188
|
+
const artifacts = getScheduleArtifacts(scheduleId);
|
|
189
|
+
const current = readJsonFile(artifacts.schedule_path) || {};
|
|
190
|
+
return writeJsonAtomic(artifacts.schedule_path, {
|
|
191
|
+
...current,
|
|
192
|
+
...patch,
|
|
193
|
+
schedule_id: scheduleId,
|
|
194
|
+
updated_at: nowIso()
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function compactScheduleForStatus(schedule) {
|
|
199
|
+
if (!schedule || typeof schedule !== "object" || Array.isArray(schedule)) return schedule || null;
|
|
200
|
+
const compact = clonePlain(schedule, schedule);
|
|
201
|
+
if (compact.run) compact.run = compactRecommendRunForStatus(compact.run);
|
|
202
|
+
if (compact.launch_payload?.run) {
|
|
203
|
+
compact.launch_payload = {
|
|
204
|
+
...compact.launch_payload,
|
|
205
|
+
run: compactRecommendRunForStatus(compact.launch_payload.run)
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return compact;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function isProcessAlive(pid) {
|
|
212
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
213
|
+
try {
|
|
214
|
+
process.kill(pid, 0);
|
|
215
|
+
return true;
|
|
216
|
+
} catch {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function stripScheduleArgs(args = {}) {
|
|
222
|
+
const cloned = clonePlain(args, {});
|
|
223
|
+
for (const key of [
|
|
224
|
+
"schedule_id",
|
|
225
|
+
"scheduleId",
|
|
226
|
+
"schedule_run_at",
|
|
227
|
+
"scheduleRunAt",
|
|
228
|
+
"run_at",
|
|
229
|
+
"runAt",
|
|
230
|
+
"schedule_delay_seconds",
|
|
231
|
+
"scheduleDelaySeconds",
|
|
232
|
+
"schedule_delay_minutes",
|
|
233
|
+
"scheduleDelayMinutes"
|
|
234
|
+
]) {
|
|
235
|
+
delete cloned[key];
|
|
236
|
+
}
|
|
237
|
+
return cloned;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function buildFailedSchedulePayload(error, extra = {}) {
|
|
241
|
+
return {
|
|
242
|
+
status: "FAILED",
|
|
243
|
+
schedule_created: false,
|
|
244
|
+
error: {
|
|
245
|
+
code: "RECOMMEND_SCHEDULE_FAILED",
|
|
246
|
+
message: error?.message || String(error || "Unable to schedule recommend run"),
|
|
247
|
+
retryable: true
|
|
248
|
+
},
|
|
249
|
+
...extra
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function launchScheduleWorker(scheduleId) {
|
|
254
|
+
const artifacts = getScheduleArtifacts(scheduleId);
|
|
255
|
+
fs.mkdirSync(path.dirname(artifacts.worker_stdout_path), { recursive: true });
|
|
256
|
+
const stdoutFd = fs.openSync(artifacts.worker_stdout_path, "a");
|
|
257
|
+
const stderrFd = fs.openSync(artifacts.worker_stderr_path, "a");
|
|
258
|
+
let child;
|
|
259
|
+
try {
|
|
260
|
+
child = spawnProcessImpl(process.execPath, [
|
|
261
|
+
thisFilePath,
|
|
262
|
+
SCHEDULE_WORKER_FLAG,
|
|
263
|
+
SCHEDULE_ID_FLAG,
|
|
264
|
+
scheduleId
|
|
265
|
+
], {
|
|
266
|
+
cwd: process.cwd(),
|
|
267
|
+
detached: true,
|
|
268
|
+
stdio: ["ignore", stdoutFd, stderrFd],
|
|
269
|
+
windowsHide: true,
|
|
270
|
+
env: process.env
|
|
271
|
+
});
|
|
272
|
+
} finally {
|
|
273
|
+
fs.closeSync(stdoutFd);
|
|
274
|
+
fs.closeSync(stderrFd);
|
|
275
|
+
}
|
|
276
|
+
if (typeof child?.unref === "function") child.unref();
|
|
277
|
+
return {
|
|
278
|
+
pid: child.pid || null,
|
|
279
|
+
stdoutPath: artifacts.worker_stdout_path,
|
|
280
|
+
stderrPath: artifacts.worker_stderr_path
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
232
284
|
export async function scheduleRecommendPipelineRunTool({ workspaceRoot = "", args = {} } = {}) {
|
|
233
285
|
const runArgs = stripScheduleArgs(args);
|
|
234
|
-
const
|
|
235
|
-
if (
|
|
236
|
-
return {
|
|
237
|
-
...prepared,
|
|
238
|
-
status: prepared.status || "FAILED",
|
|
239
|
-
schedule_created: false,
|
|
240
|
-
cron_ready: false,
|
|
241
|
-
message: "Recommend schedule was not created because the run payload is not READY."
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const due = parseRunAt(args);
|
|
246
|
-
if (!due.ok) {
|
|
286
|
+
const forbiddenDebugOptions = collectScheduledRecommendDebugOptions(runArgs);
|
|
287
|
+
if (forbiddenDebugOptions.length) {
|
|
247
288
|
return {
|
|
248
289
|
status: "FAILED",
|
|
249
290
|
schedule_created: false,
|
|
250
|
-
cron_ready:
|
|
291
|
+
cron_ready: false,
|
|
251
292
|
error: {
|
|
252
|
-
code: "
|
|
253
|
-
message:
|
|
293
|
+
code: "RECOMMEND_SCHEDULE_DEBUG_OPTIONS_FORBIDDEN",
|
|
294
|
+
message: `Recommend schedules cannot contain diagnostic-only run options: ${forbiddenDebugOptions.join(", ")}`,
|
|
254
295
|
retryable: false
|
|
255
296
|
},
|
|
256
|
-
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const scheduleId = createScheduleId(args.schedule_id || args.scheduleId);
|
|
261
|
-
const artifacts = getScheduleArtifacts(scheduleId);
|
|
262
|
-
const runAtIso = new Date(due.runAtMs).toISOString();
|
|
263
|
-
const createdAt = nowIso();
|
|
264
|
-
try {
|
|
265
|
-
writeJsonAtomic(artifacts.schedule_path, {
|
|
266
|
-
schedule_id: scheduleId,
|
|
267
|
-
state: "scheduled",
|
|
268
|
-
status: "scheduled",
|
|
269
|
-
created_at: createdAt,
|
|
270
|
-
updated_at: createdAt,
|
|
271
|
-
run_at: runAtIso,
|
|
272
|
-
run_at_ms: due.runAtMs,
|
|
273
|
-
time_source: due.source,
|
|
274
|
-
workspace_root: path.resolve(workspaceRoot || process.cwd()),
|
|
275
|
-
args: runArgs,
|
|
276
|
-
prepare: prepared,
|
|
277
|
-
worker_stdout_path: artifacts.worker_stdout_path,
|
|
278
|
-
worker_stderr_path: artifacts.worker_stderr_path,
|
|
279
|
-
pid: null,
|
|
280
|
-
run_id: null,
|
|
281
|
-
run: null,
|
|
282
|
-
error: null
|
|
283
|
-
});
|
|
284
|
-
} catch (error) {
|
|
285
|
-
return buildFailedSchedulePayload(error, { prepare: prepared });
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
let worker;
|
|
289
|
-
try {
|
|
290
|
-
worker = launchScheduleWorker(scheduleId);
|
|
291
|
-
} catch (error) {
|
|
292
|
-
writeSchedule(scheduleId, {
|
|
293
|
-
state: "failed",
|
|
294
|
-
status: "failed",
|
|
295
|
-
error: {
|
|
296
|
-
code: "SCHEDULE_WORKER_LAUNCH_FAILED",
|
|
297
|
-
message: error?.message || String(error || "Unable to launch schedule worker"),
|
|
298
|
-
retryable: true
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
return buildFailedSchedulePayload(error, {
|
|
302
|
-
schedule_created: true,
|
|
303
|
-
schedule_id: scheduleId,
|
|
304
|
-
schedule: readSchedule(scheduleId),
|
|
305
|
-
prepare: prepared
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const schedule = writeSchedule(scheduleId, {
|
|
310
|
-
state: "scheduled",
|
|
311
|
-
status: "scheduled",
|
|
312
|
-
pid: worker.pid,
|
|
313
|
-
worker_stdout_path: worker.stdoutPath,
|
|
314
|
-
worker_stderr_path: worker.stderrPath
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
return {
|
|
318
|
-
status: "SCHEDULED",
|
|
319
|
-
schedule_created: true,
|
|
320
|
-
cron_ready: true,
|
|
321
|
-
schedule_id: scheduleId,
|
|
322
|
-
run_at: runAtIso,
|
|
323
|
-
run_at_ms: due.runAtMs,
|
|
324
|
-
worker_pid: worker.pid,
|
|
325
|
-
worker_stdout_path: worker.stdoutPath,
|
|
326
|
-
worker_stderr_path: worker.stderrPath,
|
|
327
|
-
schedule,
|
|
328
|
-
prepare: prepared,
|
|
329
|
-
message: "Recommend run schedule created. The package-owned detached scheduler will start the prepared payload at run_at."
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
export function getRecommendScheduledRunTool({ args = {} } = {}) {
|
|
334
|
-
const scheduleId = safeIdPart(args.schedule_id || args.scheduleId);
|
|
335
|
-
if (!scheduleId) {
|
|
336
|
-
return {
|
|
337
|
-
status: "FAILED",
|
|
338
|
-
error: {
|
|
339
|
-
code: "INVALID_SCHEDULE_ID",
|
|
340
|
-
message: "schedule_id is required",
|
|
341
|
-
retryable: false
|
|
342
|
-
}
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
const schedule = readSchedule(scheduleId);
|
|
346
|
-
if (!schedule) {
|
|
347
|
-
return {
|
|
348
|
-
status: "FAILED",
|
|
349
|
-
error: {
|
|
350
|
-
code: "SCHEDULE_NOT_FOUND",
|
|
351
|
-
message: `schedule_id=${scheduleId} not found`,
|
|
352
|
-
retryable: false
|
|
353
|
-
}
|
|
297
|
+
forbidden_debug_options: forbiddenDebugOptions
|
|
354
298
|
};
|
|
355
299
|
}
|
|
356
|
-
|
|
357
|
-
if (
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
status: "
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
})
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
}
|
|
496
|
-
}
|
|
300
|
+
const prepared = prepareRecommendPipelineRunTool({ workspaceRoot, args: runArgs });
|
|
301
|
+
if (prepared.status !== "READY" || prepared.cron_ready !== true) {
|
|
302
|
+
return {
|
|
303
|
+
...prepared,
|
|
304
|
+
status: prepared.status || "FAILED",
|
|
305
|
+
schedule_created: false,
|
|
306
|
+
cron_ready: false,
|
|
307
|
+
message: "Recommend schedule was not created because the run payload is not READY."
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const due = parseRunAt(args);
|
|
312
|
+
if (!due.ok) {
|
|
313
|
+
return {
|
|
314
|
+
status: "FAILED",
|
|
315
|
+
schedule_created: false,
|
|
316
|
+
cron_ready: true,
|
|
317
|
+
error: {
|
|
318
|
+
code: "INVALID_SCHEDULE_TIME",
|
|
319
|
+
message: due.error,
|
|
320
|
+
retryable: false
|
|
321
|
+
},
|
|
322
|
+
prepare: prepared
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const scheduleId = createScheduleId(args.schedule_id || args.scheduleId);
|
|
327
|
+
const artifacts = getScheduleArtifacts(scheduleId);
|
|
328
|
+
const runAtIso = new Date(due.runAtMs).toISOString();
|
|
329
|
+
const createdAt = nowIso();
|
|
330
|
+
try {
|
|
331
|
+
writeJsonAtomic(artifacts.schedule_path, {
|
|
332
|
+
schedule_id: scheduleId,
|
|
333
|
+
state: "scheduled",
|
|
334
|
+
status: "scheduled",
|
|
335
|
+
created_at: createdAt,
|
|
336
|
+
updated_at: createdAt,
|
|
337
|
+
run_at: runAtIso,
|
|
338
|
+
run_at_ms: due.runAtMs,
|
|
339
|
+
time_source: due.source,
|
|
340
|
+
workspace_root: path.resolve(workspaceRoot || process.cwd()),
|
|
341
|
+
args: runArgs,
|
|
342
|
+
prepare: prepared,
|
|
343
|
+
worker_stdout_path: artifacts.worker_stdout_path,
|
|
344
|
+
worker_stderr_path: artifacts.worker_stderr_path,
|
|
345
|
+
pid: null,
|
|
346
|
+
run_id: null,
|
|
347
|
+
run: null,
|
|
348
|
+
error: null
|
|
349
|
+
});
|
|
350
|
+
} catch (error) {
|
|
351
|
+
return buildFailedSchedulePayload(error, { prepare: prepared });
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
let worker;
|
|
355
|
+
try {
|
|
356
|
+
worker = launchScheduleWorker(scheduleId);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
writeSchedule(scheduleId, {
|
|
359
|
+
state: "failed",
|
|
360
|
+
status: "failed",
|
|
361
|
+
error: {
|
|
362
|
+
code: "SCHEDULE_WORKER_LAUNCH_FAILED",
|
|
363
|
+
message: error?.message || String(error || "Unable to launch schedule worker"),
|
|
364
|
+
retryable: true
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
return buildFailedSchedulePayload(error, {
|
|
368
|
+
schedule_created: true,
|
|
369
|
+
schedule_id: scheduleId,
|
|
370
|
+
schedule: readSchedule(scheduleId),
|
|
371
|
+
prepare: prepared
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const schedule = writeSchedule(scheduleId, {
|
|
376
|
+
state: "scheduled",
|
|
377
|
+
status: "scheduled",
|
|
378
|
+
pid: worker.pid,
|
|
379
|
+
worker_stdout_path: worker.stdoutPath,
|
|
380
|
+
worker_stderr_path: worker.stderrPath
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
status: "SCHEDULED",
|
|
385
|
+
schedule_created: true,
|
|
386
|
+
cron_ready: true,
|
|
387
|
+
schedule_id: scheduleId,
|
|
388
|
+
run_at: runAtIso,
|
|
389
|
+
run_at_ms: due.runAtMs,
|
|
390
|
+
worker_pid: worker.pid,
|
|
391
|
+
worker_stdout_path: worker.stdoutPath,
|
|
392
|
+
worker_stderr_path: worker.stderrPath,
|
|
393
|
+
schedule,
|
|
394
|
+
prepare: prepared,
|
|
395
|
+
message: "Recommend run schedule created. The package-owned detached scheduler will start the prepared payload at run_at."
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function getRecommendScheduledRunTool({ args = {} } = {}) {
|
|
400
|
+
const scheduleId = safeIdPart(args.schedule_id || args.scheduleId);
|
|
401
|
+
if (!scheduleId) {
|
|
402
|
+
return {
|
|
403
|
+
status: "FAILED",
|
|
404
|
+
error: {
|
|
405
|
+
code: "INVALID_SCHEDULE_ID",
|
|
406
|
+
message: "schedule_id is required",
|
|
407
|
+
retryable: false
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const schedule = readSchedule(scheduleId);
|
|
412
|
+
if (!schedule) {
|
|
413
|
+
return {
|
|
414
|
+
status: "FAILED",
|
|
415
|
+
error: {
|
|
416
|
+
code: "SCHEDULE_NOT_FOUND",
|
|
417
|
+
message: `schedule_id=${scheduleId} not found`,
|
|
418
|
+
retryable: false
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
let next = schedule;
|
|
423
|
+
if (!TERMINAL_SCHEDULE_STATES.has(normalizeText(schedule.state || schedule.status)) && schedule.pid && !isProcessAlive(schedule.pid)) {
|
|
424
|
+
next = writeSchedule(scheduleId, {
|
|
425
|
+
state: "failed",
|
|
426
|
+
status: "failed",
|
|
427
|
+
completed_at: nowIso(),
|
|
428
|
+
error: {
|
|
429
|
+
code: "SCHEDULE_WORKER_EXITED",
|
|
430
|
+
message: `Scheduled worker process exited before reaching a terminal state (pid=${schedule.pid}).`,
|
|
431
|
+
retryable: true
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
status: "OK",
|
|
437
|
+
schedule_id: scheduleId,
|
|
438
|
+
schedule: compactScheduleForStatus(next)
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export async function runScheduledRecommendWorker({ scheduleId }) {
|
|
443
|
+
const normalizedScheduleId = safeIdPart(scheduleId);
|
|
444
|
+
if (!normalizedScheduleId) return { ok: false, error: "schedule_id is required" };
|
|
445
|
+
let schedule = readSchedule(normalizedScheduleId);
|
|
446
|
+
if (!schedule) return { ok: false, error: `schedule_id=${normalizedScheduleId} not found` };
|
|
447
|
+
const runAtMs = Number(schedule.run_at_ms);
|
|
448
|
+
if (!Number.isFinite(runAtMs)) {
|
|
449
|
+
writeSchedule(normalizedScheduleId, {
|
|
450
|
+
state: "failed",
|
|
451
|
+
status: "failed",
|
|
452
|
+
completed_at: nowIso(),
|
|
453
|
+
error: {
|
|
454
|
+
code: "INVALID_SCHEDULE_STATE",
|
|
455
|
+
message: "schedule is missing a valid run_at_ms",
|
|
456
|
+
retryable: false
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
return { ok: false, error: "schedule is missing a valid run_at_ms" };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
schedule = writeSchedule(normalizedScheduleId, {
|
|
463
|
+
state: "waiting",
|
|
464
|
+
status: "waiting",
|
|
465
|
+
pid: process.pid,
|
|
466
|
+
worker_started_at: nowIso()
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
while (Date.now() < runAtMs) {
|
|
470
|
+
await sleep(Math.min(30_000, Math.max(50, runAtMs - Date.now())));
|
|
471
|
+
const latest = readSchedule(normalizedScheduleId);
|
|
472
|
+
if (normalizeText(latest?.state) === "canceled") return { ok: true, canceled: true };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
writeSchedule(normalizedScheduleId, {
|
|
476
|
+
state: "launching",
|
|
477
|
+
status: "launching",
|
|
478
|
+
launch_started_at: nowIso()
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const started = await startRecommendPipelineRunTool({
|
|
482
|
+
workspaceRoot: schedule.workspace_root,
|
|
483
|
+
args: clonePlain(schedule.args, {})
|
|
484
|
+
});
|
|
485
|
+
if (started.status !== "ACCEPTED") {
|
|
486
|
+
writeSchedule(normalizedScheduleId, {
|
|
487
|
+
state: "failed",
|
|
488
|
+
status: "failed",
|
|
489
|
+
completed_at: nowIso(),
|
|
490
|
+
launch_payload: started,
|
|
491
|
+
error: started.error || {
|
|
492
|
+
code: "RECOMMEND_START_NOT_ACCEPTED",
|
|
493
|
+
message: started.status || "start_recommend_pipeline_run did not return ACCEPTED",
|
|
494
|
+
retryable: true
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
return { ok: false, error: started.error?.message || started.status || "not accepted" };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
writeSchedule(normalizedScheduleId, {
|
|
501
|
+
state: "running",
|
|
502
|
+
status: "running",
|
|
503
|
+
run_id: started.run_id,
|
|
504
|
+
run: started.run || null,
|
|
505
|
+
launch_payload: started,
|
|
506
|
+
launched_at: nowIso()
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
while (true) {
|
|
510
|
+
const payload = getRecommendPipelineRunTool({ args: { run_id: started.run_id } });
|
|
511
|
+
const runState = normalizeText(payload?.run?.state || payload?.run?.status);
|
|
512
|
+
writeSchedule(normalizedScheduleId, {
|
|
513
|
+
state: runState && TERMINAL_RUN_STATES.has(runState) ? runState : "running",
|
|
514
|
+
status: runState && TERMINAL_RUN_STATES.has(runState) ? runState : "running",
|
|
515
|
+
run_id: started.run_id,
|
|
516
|
+
run: payload?.run || null,
|
|
517
|
+
last_poll_at: nowIso(),
|
|
518
|
+
completed_at: runState && TERMINAL_RUN_STATES.has(runState) ? nowIso() : undefined,
|
|
519
|
+
error: runState === "failed" ? (payload?.run?.error || payload?.error || null) : null
|
|
520
|
+
});
|
|
521
|
+
if (TERMINAL_RUN_STATES.has(runState)) break;
|
|
522
|
+
await sleep(1000);
|
|
523
|
+
}
|
|
524
|
+
return { ok: true, run_id: started.run_id };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export function __setRecommendSchedulerSpawnForTests(nextImpl) {
|
|
528
|
+
spawnProcessImpl = typeof nextImpl === "function" ? nextImpl : spawn;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function parseScheduleWorkerOptions(argv = process.argv.slice(2)) {
|
|
532
|
+
if (!Array.isArray(argv) || !argv.includes(SCHEDULE_WORKER_FLAG)) return null;
|
|
533
|
+
const idIndex = argv.indexOf(SCHEDULE_ID_FLAG);
|
|
534
|
+
return {
|
|
535
|
+
scheduleId: idIndex >= 0 ? normalizeText(argv[idIndex + 1]) : ""
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const thisFilePath = fileURLToPath(import.meta.url);
|
|
540
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === thisFilePath) {
|
|
541
|
+
const options = parseScheduleWorkerOptions(process.argv.slice(2));
|
|
542
|
+
if (options) {
|
|
543
|
+
runScheduledRecommendWorker(options).then((result) => {
|
|
544
|
+
if (!result?.ok) process.exitCode = 1;
|
|
545
|
+
}).catch((error) => {
|
|
546
|
+
try {
|
|
547
|
+
writeSchedule(options.scheduleId, {
|
|
548
|
+
state: "failed",
|
|
549
|
+
status: "failed",
|
|
550
|
+
completed_at: nowIso(),
|
|
551
|
+
error: {
|
|
552
|
+
code: "SCHEDULE_WORKER_UNHANDLED_ERROR",
|
|
553
|
+
message: error?.message || String(error || "schedule worker failed"),
|
|
554
|
+
retryable: true
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
} catch {}
|
|
558
|
+
console.error("[boss-recommend-mcp] scheduled recommend worker failed", error);
|
|
559
|
+
process.exitCode = 1;
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|