@reconcrap/boss-recommend-mcp 2.0.36 → 2.0.38
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 +119 -119
- package/src/core/run/index.js +7 -2
- package/src/domains/recommend/detail.js +61 -18
- package/src/domains/recommend/run-service.js +4 -0
- package/src/index.js +429 -42
- package/src/recommend-mcp.js +216 -49
- package/src/run-state.js +2 -0
package/src/index.js
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
getRecommendPipelineRunTool,
|
|
47
47
|
listRecommendJobsTool,
|
|
48
48
|
pauseRecommendPipelineRunTool,
|
|
49
|
+
prepareRecommendPipelineRunTool,
|
|
49
50
|
resumeRecommendPipelineRunTool,
|
|
50
51
|
startRecommendPipelineRunTool
|
|
51
52
|
} from "./recommend-mcp.js";
|
|
@@ -116,6 +117,16 @@ const FRAMING_LINE = "line";
|
|
|
116
117
|
const DETACHED_WORKER_FLAG = "--detached-worker";
|
|
117
118
|
const DETACHED_WORKER_RUN_ID_FLAG = "--run-id";
|
|
118
119
|
const DETACHED_WORKER_RESUME_FLAG = "--resume";
|
|
120
|
+
const AGENT_RUNTIME_HINT_KEYS = [
|
|
121
|
+
"CODEX_CI",
|
|
122
|
+
"CODEX_THREAD_ID",
|
|
123
|
+
"CODEX_HOME",
|
|
124
|
+
"OPENCLAW_HOME",
|
|
125
|
+
"OPENCLAW",
|
|
126
|
+
"TRAE_CN",
|
|
127
|
+
"TRAE_HOME",
|
|
128
|
+
"TRAE_AGENT"
|
|
129
|
+
];
|
|
119
130
|
const featuredCalibrationUnsupportedCode = "FEATURED_CALIBRATION_UNSUPPORTED_CDP_ONLY";
|
|
120
131
|
const recommendSelfHealApplyUnsupportedCode = "RECOMMEND_SELF_HEAL_APPLY_UNSUPPORTED_CDP_ONLY";
|
|
121
132
|
const detachedLegacyPipelineUnsupportedCode = "DETACHED_LEGACY_PIPELINE_UNSUPPORTED_CDP_ONLY";
|
|
@@ -138,6 +149,31 @@ function normalizeText(value) {
|
|
|
138
149
|
return String(value || "").replace(/\s+/g, " ").trim();
|
|
139
150
|
}
|
|
140
151
|
|
|
152
|
+
function clonePlain(value, fallback = null) {
|
|
153
|
+
try {
|
|
154
|
+
return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
|
|
155
|
+
} catch {
|
|
156
|
+
return fallback;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isLikelyAgentRuntime() {
|
|
161
|
+
for (const key of AGENT_RUNTIME_HINT_KEYS) {
|
|
162
|
+
if (normalizeText(process.env[key] || "")) return true;
|
|
163
|
+
}
|
|
164
|
+
const originHints = [
|
|
165
|
+
normalizeText(process.env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE || ""),
|
|
166
|
+
normalizeText(process.env.TERM_PROGRAM || "")
|
|
167
|
+
].join(" ").toLowerCase();
|
|
168
|
+
return /codex|openclaw|trae/.test(originHints);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function shouldStartRecommendDetached() {
|
|
172
|
+
if (normalizeText(process.env.BOSS_RECOMMEND_CDP_INPROC || "") === "1") return false;
|
|
173
|
+
if (normalizeText(process.env.BOSS_RECOMMEND_CDP_DETACHED || "") === "1") return true;
|
|
174
|
+
return isLikelyAgentRuntime();
|
|
175
|
+
}
|
|
176
|
+
|
|
141
177
|
function isUnlimitedTargetCountToken(value) {
|
|
142
178
|
const token = normalizeText(value).toLowerCase();
|
|
143
179
|
if (!token) return false;
|
|
@@ -255,17 +291,67 @@ function getDefaultAcceptedMessage(args = {}) {
|
|
|
255
291
|
return `异步流水线已启动(detached)。默认不自动轮询;如需进度请按需调用 get_recommend_pipeline_run(建议至少每 ${recommendedMinutes} 分钟查询一次)。`;
|
|
256
292
|
}
|
|
257
293
|
|
|
258
|
-
function getRunArtifacts(runId) {
|
|
259
|
-
const normalizedRunId = normalizeText(runId);
|
|
260
|
-
return {
|
|
261
|
-
run_state_path: path.join(getRunsDir(), `${normalizedRunId}.json`),
|
|
262
|
-
checkpoint_path: path.join(getRunsDir(), `${normalizedRunId}.checkpoint.json`)
|
|
263
|
-
|
|
264
|
-
}
|
|
294
|
+
function getRunArtifacts(runId) {
|
|
295
|
+
const normalizedRunId = normalizeText(runId);
|
|
296
|
+
return {
|
|
297
|
+
run_state_path: path.join(getRunsDir(), `${normalizedRunId}.json`),
|
|
298
|
+
checkpoint_path: path.join(getRunsDir(), `${normalizedRunId}.checkpoint.json`),
|
|
299
|
+
worker_stdout_path: path.join(getRunsDir(), `${normalizedRunId}.worker.stdout.log`),
|
|
300
|
+
worker_stderr_path: path.join(getRunsDir(), `${normalizedRunId}.worker.stderr.log`)
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function writeRawRunState(runId, payload) {
|
|
305
|
+
const artifacts = getRunArtifacts(runId);
|
|
306
|
+
fs.mkdirSync(path.dirname(artifacts.run_state_path), { recursive: true });
|
|
307
|
+
const tempPath = `${artifacts.run_state_path}.tmp`;
|
|
308
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
309
|
+
fs.renameSync(tempPath, artifacts.run_state_path);
|
|
310
|
+
return payload;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function readRawRunState(runId) {
|
|
314
|
+
const artifacts = getRunArtifacts(runId);
|
|
315
|
+
try {
|
|
316
|
+
const parsed = JSON.parse(fs.readFileSync(artifacts.run_state_path, "utf8"));
|
|
317
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
318
|
+
} catch {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function patchRawRunState(runId, patch) {
|
|
324
|
+
const current = readRawRunState(runId);
|
|
325
|
+
if (!current) return null;
|
|
326
|
+
const now = new Date().toISOString();
|
|
327
|
+
const next = {
|
|
328
|
+
...current,
|
|
329
|
+
...patch,
|
|
330
|
+
run_id: current.run_id || runId,
|
|
331
|
+
updated_at: now,
|
|
332
|
+
heartbeat_at: current.heartbeat_at || now,
|
|
333
|
+
control: {
|
|
334
|
+
...(current.control || {}),
|
|
335
|
+
...(patch.control || {})
|
|
336
|
+
},
|
|
337
|
+
resume: {
|
|
338
|
+
...(current.resume || {}),
|
|
339
|
+
...(patch.resume || {})
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
return writeRawRunState(runId, next);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function createDetachedRecommendRunId() {
|
|
346
|
+
const suffix = Math.random().toString(36).slice(2, 10);
|
|
347
|
+
return `mcp_recommend_${Date.now().toString(36)}_${suffix}`;
|
|
348
|
+
}
|
|
265
349
|
|
|
266
350
|
function buildRunContext(workspaceRoot, args = {}) {
|
|
351
|
+
const clonedArgs = clonePlain(args, {});
|
|
267
352
|
return {
|
|
268
353
|
workspace_root: path.resolve(workspaceRoot),
|
|
354
|
+
args: clonedArgs,
|
|
269
355
|
instruction: String(args?.instruction || ""),
|
|
270
356
|
confirmation: args?.confirmation && typeof args.confirmation === "object" ? args.confirmation : {},
|
|
271
357
|
overrides: args?.overrides && typeof args.overrides === "object" ? args.overrides : {},
|
|
@@ -273,25 +359,40 @@ function buildRunContext(workspaceRoot, args = {}) {
|
|
|
273
359
|
};
|
|
274
360
|
}
|
|
275
361
|
|
|
276
|
-
function resolveRunContext(snapshot) {
|
|
277
|
-
const workspaceRoot = normalizeText(snapshot?.context?.workspace_root || "");
|
|
278
|
-
const
|
|
279
|
-
? snapshot.context.
|
|
280
|
-
:
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
362
|
+
function resolveRunContext(snapshot) {
|
|
363
|
+
const workspaceRoot = normalizeText(snapshot?.context?.workspace_root || "");
|
|
364
|
+
const storedArgs = snapshot?.context?.args && typeof snapshot.context.args === "object" && !Array.isArray(snapshot.context.args)
|
|
365
|
+
? clonePlain(snapshot.context.args, {})
|
|
366
|
+
: null;
|
|
367
|
+
const instruction = typeof storedArgs?.instruction === "string"
|
|
368
|
+
? storedArgs.instruction
|
|
369
|
+
: typeof snapshot?.context?.instruction === "string"
|
|
370
|
+
? snapshot.context.instruction
|
|
371
|
+
: "";
|
|
372
|
+
const confirmation = storedArgs?.confirmation && typeof storedArgs.confirmation === "object" && !Array.isArray(storedArgs.confirmation)
|
|
373
|
+
? storedArgs.confirmation
|
|
374
|
+
: snapshot?.context?.confirmation && typeof snapshot.context.confirmation === "object"
|
|
375
|
+
? snapshot.context.confirmation
|
|
376
|
+
: {};
|
|
377
|
+
const overrides = storedArgs?.overrides && typeof storedArgs.overrides === "object" && !Array.isArray(storedArgs.overrides)
|
|
378
|
+
? storedArgs.overrides
|
|
379
|
+
: snapshot?.context?.overrides && typeof snapshot.context.overrides === "object"
|
|
380
|
+
? snapshot.context.overrides
|
|
381
|
+
: {};
|
|
382
|
+
const followUp = storedArgs && Object.prototype.hasOwnProperty.call(storedArgs, "follow_up")
|
|
383
|
+
? storedArgs.follow_up
|
|
384
|
+
: snapshot?.context?.follow_up && typeof snapshot.context.follow_up === "object"
|
|
385
|
+
? snapshot.context.follow_up
|
|
386
|
+
: null;
|
|
387
|
+
if (!workspaceRoot || !instruction.trim()) return null;
|
|
388
|
+
return {
|
|
389
|
+
workspaceRoot,
|
|
390
|
+
args: {
|
|
391
|
+
...(storedArgs || {}),
|
|
285
392
|
instruction,
|
|
286
|
-
confirmation
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
overrides: snapshot?.context?.overrides && typeof snapshot.context.overrides === "object"
|
|
290
|
-
? snapshot.context.overrides
|
|
291
|
-
: {},
|
|
292
|
-
follow_up: snapshot?.context?.follow_up && typeof snapshot.context.follow_up === "object"
|
|
293
|
-
? snapshot.context.follow_up
|
|
294
|
-
: null
|
|
393
|
+
confirmation,
|
|
394
|
+
overrides,
|
|
395
|
+
follow_up: followUp
|
|
295
396
|
}
|
|
296
397
|
};
|
|
297
398
|
}
|
|
@@ -1455,18 +1556,145 @@ function launchDetachedRunWorker({ runId, resumeRun = false }) {
|
|
|
1455
1556
|
if (resumeRun) {
|
|
1456
1557
|
childArgs.push(DETACHED_WORKER_RESUME_FLAG);
|
|
1457
1558
|
}
|
|
1458
|
-
const
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1559
|
+
const artifacts = getRunArtifacts(runId);
|
|
1560
|
+
fs.mkdirSync(path.dirname(artifacts.worker_stdout_path), { recursive: true });
|
|
1561
|
+
const stdoutFd = fs.openSync(artifacts.worker_stdout_path, "a");
|
|
1562
|
+
const stderrFd = fs.openSync(artifacts.worker_stderr_path, "a");
|
|
1563
|
+
let child;
|
|
1564
|
+
try {
|
|
1565
|
+
child = spawnProcessImpl(process.execPath, childArgs, {
|
|
1566
|
+
detached: true,
|
|
1567
|
+
stdio: ["ignore", stdoutFd, stderrFd],
|
|
1568
|
+
windowsHide: true,
|
|
1569
|
+
env: process.env
|
|
1570
|
+
});
|
|
1571
|
+
} finally {
|
|
1572
|
+
fs.closeSync(stdoutFd);
|
|
1573
|
+
fs.closeSync(stderrFd);
|
|
1574
|
+
}
|
|
1575
|
+
child.workerLogPaths = {
|
|
1576
|
+
stdoutPath: artifacts.worker_stdout_path,
|
|
1577
|
+
stderrPath: artifacts.worker_stderr_path
|
|
1578
|
+
};
|
|
1464
1579
|
if (typeof child?.unref === "function") {
|
|
1465
1580
|
child.unref();
|
|
1466
1581
|
}
|
|
1467
1582
|
return child;
|
|
1468
1583
|
}
|
|
1469
1584
|
|
|
1585
|
+
function errorToDetachedWorkerPayload(error, fallbackMessage = "detached recommend worker exited unexpectedly") {
|
|
1586
|
+
const message = normalizeText(error?.message || error || fallbackMessage) || fallbackMessage;
|
|
1587
|
+
const payload = {
|
|
1588
|
+
code: normalizeText(error?.code || "") || "RUN_WORKER_UNHANDLED_EXCEPTION",
|
|
1589
|
+
message,
|
|
1590
|
+
retryable: true
|
|
1591
|
+
};
|
|
1592
|
+
if (normalizeText(error?.name || "")) {
|
|
1593
|
+
payload.name = normalizeText(error.name);
|
|
1594
|
+
}
|
|
1595
|
+
if (normalizeText(error?.stack || "")) {
|
|
1596
|
+
payload.stack = String(error.stack).slice(0, 8000);
|
|
1597
|
+
}
|
|
1598
|
+
return payload;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
function markDetachedWorkerFailed(runId, error, options = {}) {
|
|
1602
|
+
const normalizedRunId = normalizeText(runId);
|
|
1603
|
+
if (!normalizedRunId) return null;
|
|
1604
|
+
const existing = readRawRunState(normalizedRunId) || {};
|
|
1605
|
+
const existingState = normalizeText(existing.state || existing.status);
|
|
1606
|
+
if (TERMINAL_RUN_STATES.has(existingState)) return existing;
|
|
1607
|
+
|
|
1608
|
+
const now = new Date().toISOString();
|
|
1609
|
+
const artifacts = getRunArtifacts(normalizedRunId);
|
|
1610
|
+
const errorPayload = {
|
|
1611
|
+
...errorToDetachedWorkerPayload(error, options.message),
|
|
1612
|
+
...(options.code ? { code: options.code } : {})
|
|
1613
|
+
};
|
|
1614
|
+
const previousResult = existing.result && typeof existing.result === "object" ? existing.result : {};
|
|
1615
|
+
const result = {
|
|
1616
|
+
...previousResult,
|
|
1617
|
+
status: "FAILED",
|
|
1618
|
+
completion_reason: "failed",
|
|
1619
|
+
error: errorPayload,
|
|
1620
|
+
worker_stdout_path: artifacts.worker_stdout_path,
|
|
1621
|
+
worker_stderr_path: artifacts.worker_stderr_path
|
|
1622
|
+
};
|
|
1623
|
+
|
|
1624
|
+
return writeRawRunState(normalizedRunId, {
|
|
1625
|
+
...existing,
|
|
1626
|
+
run_id: normalizedRunId,
|
|
1627
|
+
mode: existing.mode || RUN_MODE_ASYNC,
|
|
1628
|
+
state: RUN_STATE_FAILED,
|
|
1629
|
+
status: RUN_STATE_FAILED,
|
|
1630
|
+
stage: existing.stage || RUN_STAGE_PREFLIGHT,
|
|
1631
|
+
started_at: existing.started_at || now,
|
|
1632
|
+
updated_at: now,
|
|
1633
|
+
heartbeat_at: now,
|
|
1634
|
+
completed_at: now,
|
|
1635
|
+
pid: Number.isInteger(existing.pid) && existing.pid > 0 ? existing.pid : process.pid,
|
|
1636
|
+
progress: existing.progress || {},
|
|
1637
|
+
last_message: errorPayload.message,
|
|
1638
|
+
context: existing.context || {},
|
|
1639
|
+
control: existing.control || {
|
|
1640
|
+
pause_requested: false,
|
|
1641
|
+
pause_requested_at: null,
|
|
1642
|
+
pause_requested_by: null,
|
|
1643
|
+
cancel_requested: false
|
|
1644
|
+
},
|
|
1645
|
+
resume: {
|
|
1646
|
+
...(existing.resume && typeof existing.resume === "object" ? existing.resume : {}),
|
|
1647
|
+
checkpoint_path: existing.resume?.checkpoint_path || artifacts.checkpoint_path,
|
|
1648
|
+
pause_control_path: existing.resume?.pause_control_path || artifacts.run_state_path,
|
|
1649
|
+
worker_stdout_path: artifacts.worker_stdout_path,
|
|
1650
|
+
worker_stderr_path: artifacts.worker_stderr_path
|
|
1651
|
+
},
|
|
1652
|
+
artifacts: {
|
|
1653
|
+
...(existing.artifacts && typeof existing.artifacts === "object" ? existing.artifacts : {}),
|
|
1654
|
+
worker_stdout_path: artifacts.worker_stdout_path,
|
|
1655
|
+
worker_stderr_path: artifacts.worker_stderr_path
|
|
1656
|
+
},
|
|
1657
|
+
error: errorPayload,
|
|
1658
|
+
result
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
function installDetachedWorkerFailureHandlers(runId) {
|
|
1663
|
+
let handled = false;
|
|
1664
|
+
const failOnce = (error, options = {}) => {
|
|
1665
|
+
if (handled) return;
|
|
1666
|
+
handled = true;
|
|
1667
|
+
try {
|
|
1668
|
+
markDetachedWorkerFailed(runId, error, options);
|
|
1669
|
+
} catch (markError) {
|
|
1670
|
+
console.error("[boss-recommend-mcp] failed to persist detached worker failure", markError);
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1673
|
+
|
|
1674
|
+
process.on("uncaughtException", (error) => {
|
|
1675
|
+
console.error("[boss-recommend-mcp] detached worker uncaught exception", error);
|
|
1676
|
+
failOnce(error, { code: "RUN_WORKER_UNCAUGHT_EXCEPTION" });
|
|
1677
|
+
process.exit(1);
|
|
1678
|
+
});
|
|
1679
|
+
|
|
1680
|
+
process.on("unhandledRejection", (reason) => {
|
|
1681
|
+
console.error("[boss-recommend-mcp] detached worker unhandled rejection", reason);
|
|
1682
|
+
const error = reason instanceof Error ? reason : new Error(normalizeText(reason) || "Unhandled promise rejection");
|
|
1683
|
+
failOnce(error, { code: "RUN_WORKER_UNHANDLED_REJECTION" });
|
|
1684
|
+
process.exit(1);
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
1688
|
+
process.on(signal, () => {
|
|
1689
|
+
const error = new Error(`detached recommend worker received ${signal}`);
|
|
1690
|
+
console.error("[boss-recommend-mcp] detached worker received signal", signal);
|
|
1691
|
+
failOnce(error, { code: "RUN_WORKER_SIGNAL" });
|
|
1692
|
+
const signalExitCodes = { SIGHUP: 129, SIGINT: 130, SIGTERM: 143 };
|
|
1693
|
+
process.exit(signalExitCodes[signal] || 1);
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1470
1698
|
function buildWorkerLaunchFailedPayload(message) {
|
|
1471
1699
|
return {
|
|
1472
1700
|
status: "FAILED",
|
|
@@ -1827,35 +2055,191 @@ async function runDetachedWorker({ runId, resumeRun = false, workerPid = process
|
|
|
1827
2055
|
: "detached worker 已启动,准备执行。"
|
|
1828
2056
|
});
|
|
1829
2057
|
|
|
1830
|
-
await
|
|
1831
|
-
runId: normalizedRunId,
|
|
1832
|
-
mode: RUN_MODE_ASYNC,
|
|
2058
|
+
const started = await startRecommendPipelineRunTool({
|
|
1833
2059
|
workspaceRoot: executionContext.workspaceRoot,
|
|
1834
2060
|
args: executionContext.args,
|
|
1835
|
-
|
|
1836
|
-
resumeRun
|
|
2061
|
+
runId: normalizedRunId
|
|
1837
2062
|
});
|
|
2063
|
+
if (started?.status !== "ACCEPTED") {
|
|
2064
|
+
const failedPayload = started?.error || {
|
|
2065
|
+
code: "RUN_WORKER_START_FAILED",
|
|
2066
|
+
message: started?.status || "detached recommend worker failed to start",
|
|
2067
|
+
retryable: true
|
|
2068
|
+
};
|
|
2069
|
+
safeUpdateRunState(normalizedRunId, {
|
|
2070
|
+
state: RUN_STATE_FAILED,
|
|
2071
|
+
stage: snapshot.stage || RUN_STAGE_PREFLIGHT,
|
|
2072
|
+
last_message: failedPayload.message,
|
|
2073
|
+
error: failedPayload,
|
|
2074
|
+
result: {
|
|
2075
|
+
status: "FAILED",
|
|
2076
|
+
error: failedPayload
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
2079
|
+
return { ok: false, error: failedPayload.message };
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
while (true) {
|
|
2083
|
+
const payload = getRecommendPipelineRunTool({ args: { run_id: normalizedRunId } });
|
|
2084
|
+
const state = normalizeText(payload?.run?.state || payload?.run?.status || "");
|
|
2085
|
+
if (TERMINAL_RUN_STATES.has(state)) break;
|
|
2086
|
+
const persisted = readRawRunState(normalizedRunId);
|
|
2087
|
+
if (persisted?.control?.cancel_requested === true) {
|
|
2088
|
+
cancelRecommendPipelineRunTool({ args: { run_id: normalizedRunId } });
|
|
2089
|
+
} else if (persisted?.control?.pause_requested === true && state === RUN_STATE_RUNNING) {
|
|
2090
|
+
pauseRecommendPipelineRunTool({ args: { run_id: normalizedRunId } });
|
|
2091
|
+
} else if (persisted?.control?.pause_requested === false && state === RUN_STATE_PAUSED) {
|
|
2092
|
+
resumeRecommendPipelineRunTool({ args: { run_id: normalizedRunId } });
|
|
2093
|
+
}
|
|
2094
|
+
await sleepMs(1000);
|
|
2095
|
+
}
|
|
1838
2096
|
return { ok: true };
|
|
1839
2097
|
}
|
|
1840
|
-
|
|
2098
|
+
|
|
1841
2099
|
async function handleStartRunTool({ workspaceRoot, args }) {
|
|
1842
|
-
|
|
2100
|
+
if (!shouldStartRecommendDetached()) {
|
|
2101
|
+
return startRecommendPipelineRunTool({ workspaceRoot, args });
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
const prepared = prepareRecommendPipelineRunTool({ workspaceRoot, args });
|
|
2105
|
+
if (prepared.status !== "READY") return prepared;
|
|
2106
|
+
|
|
2107
|
+
cleanupExpiredRuns();
|
|
2108
|
+
const runId = createDetachedRecommendRunId();
|
|
2109
|
+
try {
|
|
2110
|
+
initializeRunStateOrThrow(runId, RUN_MODE_ASYNC, workspaceRoot, args, process.pid);
|
|
2111
|
+
} catch (error) {
|
|
2112
|
+
return {
|
|
2113
|
+
status: "FAILED",
|
|
2114
|
+
error: {
|
|
2115
|
+
code: "RUN_STATE_IO_ERROR",
|
|
2116
|
+
message: `无法写入运行状态目录:${error.message || "unknown"}`,
|
|
2117
|
+
retryable: false
|
|
2118
|
+
}
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
let worker;
|
|
2123
|
+
try {
|
|
2124
|
+
worker = launchDetachedRunWorker({ runId });
|
|
2125
|
+
const workerLogPaths = worker.workerLogPaths || {};
|
|
2126
|
+
safeUpdateRunState(runId, {
|
|
2127
|
+
pid: worker.pid || process.pid,
|
|
2128
|
+
resume: {
|
|
2129
|
+
worker_stdout_path: workerLogPaths.stdoutPath || getRunArtifacts(runId).worker_stdout_path,
|
|
2130
|
+
worker_stderr_path: workerLogPaths.stderrPath || getRunArtifacts(runId).worker_stderr_path
|
|
2131
|
+
}
|
|
2132
|
+
});
|
|
2133
|
+
} catch (error) {
|
|
2134
|
+
const failed = buildWorkerLaunchFailedPayload(error?.message || "无法启动 detached recommend worker。");
|
|
2135
|
+
safeUpdateRunState(runId, {
|
|
2136
|
+
state: RUN_STATE_FAILED,
|
|
2137
|
+
stage: RUN_STAGE_PREFLIGHT,
|
|
2138
|
+
last_message: failed.error.message,
|
|
2139
|
+
error: failed.error,
|
|
2140
|
+
result: failed
|
|
2141
|
+
});
|
|
2142
|
+
return failed;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
const run = readRunState(runId);
|
|
2146
|
+
return {
|
|
2147
|
+
status: "ACCEPTED",
|
|
2148
|
+
run_id: runId,
|
|
2149
|
+
state: "queued",
|
|
2150
|
+
run,
|
|
2151
|
+
poll_after_sec: getRecommendedPollAfterSec(args),
|
|
2152
|
+
message: getDefaultAcceptedMessage(args),
|
|
2153
|
+
post_action: prepared.post_action,
|
|
2154
|
+
target_count_semantics: prepared.target_count_semantics,
|
|
2155
|
+
review: prepared.review
|
|
2156
|
+
};
|
|
1843
2157
|
}
|
|
1844
2158
|
|
|
1845
2159
|
function handleGetRunTool(args) {
|
|
1846
2160
|
return getRecommendPipelineRunTool({ args });
|
|
1847
2161
|
}
|
|
1848
2162
|
|
|
2163
|
+
function patchDetachedRecommendControl(args, controlPatch, {
|
|
2164
|
+
status,
|
|
2165
|
+
message,
|
|
2166
|
+
lastMessage
|
|
2167
|
+
} = {}) {
|
|
2168
|
+
const runId = normalizeText(args?.run_id || args?.runId || "");
|
|
2169
|
+
if (!runId) return null;
|
|
2170
|
+
const current = readRawRunState(runId);
|
|
2171
|
+
const state = normalizeText(current?.state || current?.status || "");
|
|
2172
|
+
if (!current || TERMINAL_RUN_STATES.has(state)) return null;
|
|
2173
|
+
const patched = patchRawRunState(runId, {
|
|
2174
|
+
last_message: lastMessage || message || current.last_message || "",
|
|
2175
|
+
control: controlPatch
|
|
2176
|
+
});
|
|
2177
|
+
if (!patched) return null;
|
|
2178
|
+
return {
|
|
2179
|
+
status,
|
|
2180
|
+
run: patched,
|
|
2181
|
+
message,
|
|
2182
|
+
persistence: {
|
|
2183
|
+
source: "disk",
|
|
2184
|
+
active_control_available: false,
|
|
2185
|
+
detached_control_requested: true
|
|
2186
|
+
},
|
|
2187
|
+
runtime_evaluate_used: false,
|
|
2188
|
+
method_summary: {},
|
|
2189
|
+
method_log: [],
|
|
2190
|
+
chrome: null
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
|
|
1849
2194
|
function handleCancelRunTool(args) {
|
|
1850
|
-
|
|
2195
|
+
const result = cancelRecommendPipelineRunTool({ args });
|
|
2196
|
+
if (result?.status === "RUN_STATUS" && result?.persistence?.active_control_available === false) {
|
|
2197
|
+
return patchDetachedRecommendControl(args, {
|
|
2198
|
+
pause_requested: true,
|
|
2199
|
+
pause_requested_at: new Date().toISOString(),
|
|
2200
|
+
pause_requested_by: TOOL_CANCEL_RUN,
|
|
2201
|
+
cancel_requested: true
|
|
2202
|
+
}, {
|
|
2203
|
+
status: "CANCEL_REQUESTED",
|
|
2204
|
+
message: "已收到取消请求,将由 detached worker 在下一个安全边界停止。",
|
|
2205
|
+
lastMessage: "已收到取消请求,将由 detached worker 在下一个安全边界停止。"
|
|
2206
|
+
}) || result;
|
|
2207
|
+
}
|
|
2208
|
+
return result;
|
|
1851
2209
|
}
|
|
1852
2210
|
|
|
1853
2211
|
function handlePauseRunTool(args) {
|
|
1854
|
-
|
|
2212
|
+
const result = pauseRecommendPipelineRunTool({ args });
|
|
2213
|
+
if (result?.status === "RUN_STATUS" && result?.persistence?.active_control_available === false) {
|
|
2214
|
+
return patchDetachedRecommendControl(args, {
|
|
2215
|
+
pause_requested: true,
|
|
2216
|
+
pause_requested_at: new Date().toISOString(),
|
|
2217
|
+
pause_requested_by: TOOL_PAUSE_RUN,
|
|
2218
|
+
cancel_requested: false
|
|
2219
|
+
}, {
|
|
2220
|
+
status: "PAUSE_REQUESTED",
|
|
2221
|
+
message: "暂停请求已写入 detached run 控制文件。",
|
|
2222
|
+
lastMessage: "暂停请求已写入 detached run 控制文件。"
|
|
2223
|
+
}) || result;
|
|
2224
|
+
}
|
|
2225
|
+
return result;
|
|
1855
2226
|
}
|
|
1856
2227
|
|
|
1857
2228
|
function handleResumeRunTool(args) {
|
|
1858
|
-
|
|
2229
|
+
const result = resumeRecommendPipelineRunTool({ args });
|
|
2230
|
+
if (result?.status === "FAILED" && result?.error?.code === "RUN_NOT_ACTIVE") {
|
|
2231
|
+
return patchDetachedRecommendControl(args, {
|
|
2232
|
+
pause_requested: false,
|
|
2233
|
+
pause_requested_at: null,
|
|
2234
|
+
pause_requested_by: null,
|
|
2235
|
+
cancel_requested: false
|
|
2236
|
+
}, {
|
|
2237
|
+
status: "RESUME_REQUESTED",
|
|
2238
|
+
message: "恢复请求已写入 detached run 控制文件。",
|
|
2239
|
+
lastMessage: "恢复请求已写入 detached run 控制文件。"
|
|
2240
|
+
}) || result;
|
|
2241
|
+
}
|
|
2242
|
+
return result;
|
|
1859
2243
|
}
|
|
1860
2244
|
|
|
1861
2245
|
function handleGetFeaturedCalibrationStatusTool(workspaceRoot) {
|
|
@@ -2354,6 +2738,7 @@ const thisFilePath = fileURLToPath(import.meta.url);
|
|
|
2354
2738
|
if (process.argv[1] && path.resolve(process.argv[1]) === thisFilePath) {
|
|
2355
2739
|
const detachedWorkerOptions = parseDetachedWorkerOptions(process.argv.slice(2));
|
|
2356
2740
|
if (detachedWorkerOptions) {
|
|
2741
|
+
installDetachedWorkerFailureHandlers(detachedWorkerOptions.runId);
|
|
2357
2742
|
runDetachedWorker({
|
|
2358
2743
|
runId: detachedWorkerOptions.runId,
|
|
2359
2744
|
resumeRun: detachedWorkerOptions.resumeRun
|
|
@@ -2361,7 +2746,9 @@ if (process.argv[1] && path.resolve(process.argv[1]) === thisFilePath) {
|
|
|
2361
2746
|
if (!result?.ok) {
|
|
2362
2747
|
process.exitCode = 1;
|
|
2363
2748
|
}
|
|
2364
|
-
}).catch(() => {
|
|
2749
|
+
}).catch((error) => {
|
|
2750
|
+
console.error("[boss-recommend-mcp] detached worker failed", error);
|
|
2751
|
+
markDetachedWorkerFailed(detachedWorkerOptions.runId, error);
|
|
2365
2752
|
process.exitCode = 1;
|
|
2366
2753
|
});
|
|
2367
2754
|
} else {
|