@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/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 instruction = typeof snapshot?.context?.instruction === "string"
279
- ? snapshot.context.instruction
280
- : "";
281
- if (!workspaceRoot || !instruction.trim()) return null;
282
- return {
283
- workspaceRoot,
284
- args: {
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: snapshot?.context?.confirmation && typeof snapshot.context.confirmation === "object"
287
- ? snapshot.context.confirmation
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 child = spawnProcessImpl(process.execPath, childArgs, {
1459
- detached: true,
1460
- stdio: "ignore",
1461
- windowsHide: true,
1462
- env: process.env
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 executeTrackedPipeline({
1831
- runId: normalizedRunId,
1832
- mode: RUN_MODE_ASYNC,
2058
+ const started = await startRecommendPipelineRunTool({
1833
2059
  workspaceRoot: executionContext.workspaceRoot,
1834
2060
  args: executionContext.args,
1835
- signal: new AbortController().signal,
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
- return startRecommendPipelineRunTool({ workspaceRoot, args });
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
- return cancelRecommendPipelineRunTool({ args });
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
- return pauseRecommendPipelineRunTool({ args });
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
- return resumeRecommendPipelineRunTool({ args });
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 {