@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.
@@ -140,6 +140,15 @@ function clonePlain(value, fallback = null) {
140
140
  }
141
141
  }
142
142
 
143
+ function plainRecord(value) {
144
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
145
+ }
146
+
147
+ function nonEmptyRecord(value) {
148
+ const record = plainRecord(value);
149
+ return Object.keys(record).length ? record : null;
150
+ }
151
+
143
152
  function normalizeRunId(runId) {
144
153
  const normalized = normalizeText(runId);
145
154
  if (!normalized || normalized.includes("/") || normalized.includes("\\")) return "";
@@ -156,6 +165,8 @@ function getRecommendRunArtifacts(runId) {
156
165
  output_dir: outputDir,
157
166
  run_state_path: path.join(runsDir, `${normalized}.json`),
158
167
  checkpoint_path: path.join(runsDir, `${normalized}.checkpoint.json`),
168
+ worker_stdout_path: path.join(runsDir, `${normalized}.worker.stdout.log`),
169
+ worker_stderr_path: path.join(runsDir, `${normalized}.worker.stderr.log`),
159
170
  output_csv: path.join(outputDir, `${normalized}.results.csv`),
160
171
  report_json: path.join(outputDir, `${normalized}.report.json`)
161
172
  };
@@ -191,11 +202,29 @@ function recommendSearchParamsForCsv(searchParams = {}) {
191
202
  };
192
203
  }
193
204
 
194
- function selectedRecommendJobForCsv(meta = {}) {
205
+ function getSnapshotRequestContext(snapshot = {}) {
206
+ const context = plainRecord(snapshot?.context);
207
+ const shared = plainRecord(context.shared_run_context);
208
+ return {
209
+ context,
210
+ confirmation: nonEmptyRecord(context.confirmation) || plainRecord(shared.confirmation),
211
+ overrides: nonEmptyRecord(context.overrides) || plainRecord(shared.overrides),
212
+ followUp: context.follow_up ?? shared.follow_up ?? null,
213
+ shared
214
+ };
215
+ }
216
+
217
+ function selectedRecommendJobForCsv(meta = {}, snapshot = {}) {
218
+ const { confirmation, overrides, shared } = getSnapshotRequestContext(snapshot);
195
219
  const value = normalizeText(
196
220
  meta.args?.confirmation?.job_value
197
221
  || meta.normalized?.job
198
222
  || meta.args?.overrides?.job
223
+ || confirmation.job_value
224
+ || overrides.job
225
+ || shared.confirmation?.job_value
226
+ || shared.overrides?.job
227
+ || shared.job_label
199
228
  || ""
200
229
  );
201
230
  return {
@@ -206,21 +235,28 @@ function selectedRecommendJobForCsv(meta = {}) {
206
235
  }
207
236
 
208
237
  function buildRecommendCsvInputRows(snapshot = {}, meta = {}) {
209
- const searchParams = recommendSearchParamsForCsv(meta.parsed?.searchParams || {});
210
- const screenParams = meta.parsed?.screenParams || {};
238
+ const { context, confirmation, overrides, followUp, shared } = getSnapshotRequestContext(snapshot);
239
+ const searchParams = recommendSearchParamsForCsv(meta.parsed?.searchParams || {
240
+ school_tag: overrides.school_tag ?? confirmation.school_tag_value,
241
+ degree: overrides.degree ?? confirmation.degree_value,
242
+ gender: overrides.gender ?? confirmation.gender_value,
243
+ recent_not_view: overrides.recent_not_view ?? confirmation.recent_not_view_value
244
+ });
245
+ const parsedScreenParams = meta.parsed?.screenParams || {};
246
+ const screenParams = {
247
+ criteria: parsedScreenParams.criteria || meta.normalized?.criteria || overrides.criteria || "",
248
+ target_count: parsedScreenParams.target_count || snapshot.progress?.target_count || meta.normalized?.targetCount || overrides.target_count || confirmation.target_count_value || shared.max_candidates || "",
249
+ post_action: parsedScreenParams.post_action || overrides.post_action || confirmation.post_action_value || shared.post_action || "none",
250
+ max_greet_count: parsedScreenParams.max_greet_count ?? overrides.max_greet_count ?? confirmation.max_greet_count_value ?? shared.max_greet_count ?? ""
251
+ };
211
252
  return buildLegacyScreenInputRows({
212
- instruction: meta.args?.instruction || "",
253
+ instruction: meta.args?.instruction || context.instruction || shared.instruction || "",
213
254
  selectedPage: "recommend",
214
- selectedJob: selectedRecommendJobForCsv(meta),
255
+ selectedJob: selectedRecommendJobForCsv(meta, snapshot),
215
256
  userSearchParams: cloneReportInput(searchParams, {}),
216
257
  effectiveSearchParams: cloneReportInput(searchParams, {}),
217
- screenParams: {
218
- criteria: screenParams.criteria || meta.normalized?.criteria || "",
219
- target_count: screenParams.target_count || snapshot.progress?.target_count || meta.normalized?.targetCount || "",
220
- post_action: screenParams.post_action || "none",
221
- max_greet_count: screenParams.max_greet_count ?? ""
222
- },
223
- followUp: meta.args?.follow_up || meta.args?.overrides?.follow_up || null
258
+ screenParams,
259
+ followUp: meta.args?.follow_up || meta.args?.overrides?.follow_up || followUp || overrides.follow_up || null
224
260
  });
225
261
  }
226
262
 
@@ -237,6 +273,16 @@ function readRecommendRunState(runId) {
237
273
  return readJsonFile(artifacts.run_state_path);
238
274
  }
239
275
 
276
+ function isProcessAlive(pid) {
277
+ if (!Number.isInteger(pid) || pid <= 0) return false;
278
+ try {
279
+ process.kill(pid, 0);
280
+ return true;
281
+ } catch {
282
+ return false;
283
+ }
284
+ }
285
+
240
286
  function getRecommendRunMeta(runId) {
241
287
  return recommendRunMeta.get(runId) || {};
242
288
  }
@@ -475,12 +521,14 @@ function normalizeRunSnapshot(snapshot) {
475
521
  || snapshot.status === RUN_STATUS_PAUSED
476
522
  ) ? buildLegacyRecommendResult({ ...snapshot, progress }) : null;
477
523
  const recovery = buildConstrainedAgentRecovery(snapshot, meta, artifacts);
524
+ const snapshotContext = plainRecord(snapshot.context);
525
+ const metaArgs = plainRecord(meta.args);
478
526
  const oldContext = {
479
- workspace_root: meta.workspaceRoot || null,
480
- instruction: meta.args?.instruction || "",
481
- confirmation: clonePlain(meta.args?.confirmation || {}, {}),
482
- overrides: clonePlain(meta.args?.overrides || {}, {}),
483
- follow_up: clonePlain(meta.args?.follow_up || {}, {}),
527
+ workspace_root: meta.workspaceRoot || snapshotContext.workspace_root || null,
528
+ instruction: metaArgs.instruction || snapshotContext.instruction || "",
529
+ confirmation: clonePlain(metaArgs.confirmation ?? snapshotContext.confirmation ?? {}, {}),
530
+ overrides: clonePlain(metaArgs.overrides ?? snapshotContext.overrides ?? {}, {}),
531
+ follow_up: clonePlain(metaArgs.follow_up ?? snapshotContext.follow_up ?? null, null),
484
532
  target_count_semantics: TARGET_COUNT_SEMANTICS
485
533
  };
486
534
  return {
@@ -494,12 +542,12 @@ function normalizeRunSnapshot(snapshot) {
494
542
  updated_at: snapshot.updatedAt,
495
543
  completed_at: toIsoOrNull(snapshot.completedAt),
496
544
  heartbeat_at: snapshot.updatedAt,
497
- pid: process.pid || null,
545
+ pid: Number.isInteger(snapshot.pid) && snapshot.pid > 0 ? snapshot.pid : process.pid || null,
498
546
  last_message: snapshot.error?.message || snapshot.phase || null,
499
547
  context: {
500
- ...(snapshot.context || {}),
548
+ ...snapshotContext,
501
549
  ...oldContext,
502
- shared_run_context: snapshot.context || {}
550
+ shared_run_context: snapshotContext
503
551
  },
504
552
  control: {
505
553
  pause_requested: snapshot.status === RUN_STATUS_PAUSED,
@@ -511,6 +559,8 @@ function normalizeRunSnapshot(snapshot) {
511
559
  checkpoint_path: legacyResult?.checkpoint_path || meta.checkpointPath || artifacts?.checkpoint_path || null,
512
560
  pause_control_path: artifacts?.run_state_path || null,
513
561
  output_csv: legacyResult?.output_csv || null,
562
+ worker_stdout_path: artifacts?.worker_stdout_path || null,
563
+ worker_stderr_path: artifacts?.worker_stderr_path || null,
514
564
  resume_count: meta.resumeCount || 0,
515
565
  last_resumed_at: meta.lastResumedAt || null,
516
566
  last_paused_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null
@@ -521,6 +571,41 @@ function normalizeRunSnapshot(snapshot) {
521
571
  };
522
572
  }
523
573
 
574
+ function mergePersistedControlRequest(normalized, existing) {
575
+ const control = {
576
+ ...(normalized?.control || {})
577
+ };
578
+ if (!normalized || TERMINAL_STATUSES.has(normalized.state)) return control;
579
+ const existingControl = plainRecord(existing?.control);
580
+ if (existingControl.cancel_requested === true) {
581
+ return {
582
+ ...control,
583
+ pause_requested: true,
584
+ pause_requested_at: existingControl.pause_requested_at || control.pause_requested_at || new Date().toISOString(),
585
+ pause_requested_by: existingControl.pause_requested_by || control.pause_requested_by || "cancel_recommend_pipeline_run",
586
+ cancel_requested: true
587
+ };
588
+ }
589
+ if (existingControl.pause_requested === true && normalized.state !== RUN_STATUS_PAUSED) {
590
+ return {
591
+ ...control,
592
+ pause_requested: true,
593
+ pause_requested_at: existingControl.pause_requested_at || control.pause_requested_at || new Date().toISOString(),
594
+ pause_requested_by: existingControl.pause_requested_by || control.pause_requested_by || "pause_recommend_pipeline_run"
595
+ };
596
+ }
597
+ if (existingControl.pause_requested === false && normalized.state === RUN_STATUS_PAUSED) {
598
+ return {
599
+ ...control,
600
+ pause_requested: false,
601
+ pause_requested_at: null,
602
+ pause_requested_by: null,
603
+ cancel_requested: false
604
+ };
605
+ }
606
+ return control;
607
+ }
608
+
524
609
  function persistRecommendRunSnapshot(snapshot, {
525
610
  persistActiveCheckpoint = false
526
611
  } = {}) {
@@ -528,6 +613,8 @@ function persistRecommendRunSnapshot(snapshot, {
528
613
  if (!normalized?.run_id) return normalized;
529
614
  const artifacts = getRecommendRunArtifacts(normalized.run_id);
530
615
  if (!artifacts) return normalized;
616
+ const existing = readJsonFile(artifacts.run_state_path);
617
+ normalized.control = mergePersistedControlRequest(normalized, existing);
531
618
  if (persistActiveCheckpoint) {
532
619
  persistRecommendCheckpointSnapshot(normalized);
533
620
  }
@@ -557,6 +644,38 @@ function persistRecommendRunSnapshot(snapshot, {
557
644
  return normalized;
558
645
  }
559
646
 
647
+ function reconcilePersistedRecommendRunIfNeeded(persisted) {
648
+ if (!persisted || typeof persisted !== "object") return persisted;
649
+ const persistedState = normalizeText(persisted.state || persisted.status);
650
+ if (TERMINAL_STATUSES.has(persistedState)) return persisted;
651
+ if (isProcessAlive(persisted.pid)) return persisted;
652
+
653
+ const runId = normalizeRunId(persisted.run_id || persisted.runId);
654
+ const artifacts = getRecommendRunArtifacts(runId);
655
+ const checkpoint = artifacts?.checkpoint_path ? readJsonFile(artifacts.checkpoint_path) : null;
656
+ const now = new Date().toISOString();
657
+ const error = {
658
+ code: "RUN_PROCESS_EXITED",
659
+ message: `检测到推荐任务进程已退出(pid=${persisted.pid || "unknown"}),已自动标记为失败。`,
660
+ retryable: true
661
+ };
662
+ return persistRecommendRunSnapshot({
663
+ runId,
664
+ name: persisted.name || runId,
665
+ status: RUN_STATUS_FAILED,
666
+ phase: persisted.stage || persisted.phase || "recommend:orphaned",
667
+ progress: persisted.progress || {},
668
+ context: persisted.context || {},
669
+ checkpoint: checkpoint || persisted.checkpoint || {},
670
+ startedAt: persisted.started_at || persisted.startedAt || now,
671
+ updatedAt: now,
672
+ completedAt: now,
673
+ pid: Number.isInteger(persisted.pid) && persisted.pid > 0 ? persisted.pid : null,
674
+ error,
675
+ summary: persisted.summary || null
676
+ });
677
+ }
678
+
560
679
  function persistRecommendLifecycleSnapshot(snapshot, event = {}) {
561
680
  return persistRecommendRunSnapshot(snapshot, {
562
681
  persistActiveCheckpoint: event?.type === "checkpoint"
@@ -1154,6 +1273,47 @@ function getRunOptions(args, parsed, normalized, session, configResolution = nul
1154
1273
  };
1155
1274
  }
1156
1275
 
1276
+ function prepareRecommendPipelineStart(args = {}, { workspaceRoot = "" } = {}) {
1277
+ const parsed = parseRecommendPipelineRequest(args);
1278
+ const gate = evaluateRecommendPipelineGate(parsed, args);
1279
+ if (gate) return { response: gate };
1280
+ const configResolution = resolveBossScreeningConfig(workspaceRoot);
1281
+ const normalized = normalizeRecommendStartInput(args, parsed, configResolution);
1282
+ const debugTestOptions = collectRecommendDebugTestOptions(args, normalized);
1283
+ if (debugTestOptions.length && !isDebugTestMode(args)) {
1284
+ return {
1285
+ response: {
1286
+ status: "FAILED",
1287
+ error: {
1288
+ code: "DEBUG_TEST_MODE_REQUIRED",
1289
+ message: `这些参数属于调试/测试路径,正式 live run 不会默认启用:${debugTestOptions.join(", ")}。如确需测试,请显式传 debug_test_mode=true。`,
1290
+ retryable: false
1291
+ },
1292
+ debug_test_options: debugTestOptions
1293
+ }
1294
+ };
1295
+ }
1296
+ if (normalized.screeningMode === "llm" && !configResolution.ok) {
1297
+ return {
1298
+ response: {
1299
+ status: "FAILED",
1300
+ error: {
1301
+ code: "SCREEN_CONFIG_ERROR",
1302
+ message: configResolution.error?.message || "screening-config.json is required for LLM screening.",
1303
+ retryable: true
1304
+ },
1305
+ config_path: configResolution.config_path || null,
1306
+ candidate_paths: configResolution.candidate_paths || []
1307
+ }
1308
+ };
1309
+ }
1310
+ return {
1311
+ parsed,
1312
+ configResolution,
1313
+ normalized
1314
+ };
1315
+ }
1316
+
1157
1317
  async function closeRecommendRunSession(runId) {
1158
1318
  const meta = recommendRunMeta.get(runId);
1159
1319
  if (!meta || meta.closed) return;
@@ -1199,34 +1359,19 @@ function trackRecommendRun(runId) {
1199
1359
  });
1200
1360
  }
1201
1361
 
1202
- async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = "" } = {}) {
1203
- const parsed = parseRecommendPipelineRequest(args);
1204
- const gate = evaluateRecommendPipelineGate(parsed, args);
1205
- if (gate) return gate;
1206
- const configResolution = resolveBossScreeningConfig(workspaceRoot);
1207
- const normalized = normalizeRecommendStartInput(args, parsed, configResolution);
1208
- const debugTestOptions = collectRecommendDebugTestOptions(args, normalized);
1209
- if (debugTestOptions.length && !isDebugTestMode(args)) {
1362
+ async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = "", runId = "" } = {}) {
1363
+ const prepared = prepareRecommendPipelineStart(args, { workspaceRoot });
1364
+ if (prepared.response) return prepared.response;
1365
+ const { parsed, configResolution, normalized } = prepared;
1366
+ const fixedRunId = normalizeRunId(runId);
1367
+ if (runId && !fixedRunId) {
1210
1368
  return {
1211
1369
  status: "FAILED",
1212
1370
  error: {
1213
- code: "DEBUG_TEST_MODE_REQUIRED",
1214
- message: `这些参数属于调试/测试路径,正式 live run 不会默认启用:${debugTestOptions.join(", ")}。如确需测试,请显式传 debug_test_mode=true。`,
1371
+ code: "INVALID_RUN_ID",
1372
+ message: "run_id is invalid",
1215
1373
  retryable: false
1216
- },
1217
- debug_test_options: debugTestOptions
1218
- };
1219
- }
1220
- if (normalized.screeningMode === "llm" && !configResolution.ok) {
1221
- return {
1222
- status: "FAILED",
1223
- error: {
1224
- code: "SCREEN_CONFIG_ERROR",
1225
- message: configResolution.error?.message || "screening-config.json is required for LLM screening.",
1226
- retryable: true
1227
- },
1228
- config_path: configResolution.config_path || null,
1229
- candidate_paths: configResolution.candidate_paths || []
1374
+ }
1230
1375
  };
1231
1376
  }
1232
1377
 
@@ -1260,7 +1405,11 @@ async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = ""
1260
1405
 
1261
1406
  let started;
1262
1407
  try {
1263
- started = recommendRunService.startRecommendRun(getRunOptions(args, parsed, normalized, session, configResolution));
1408
+ started = recommendRunService.startRecommendRun({
1409
+ ...getRunOptions(args, parsed, normalized, session, configResolution),
1410
+ runId: fixedRunId || undefined,
1411
+ pid: process.pid
1412
+ });
1264
1413
  } catch (error) {
1265
1414
  await session.close?.();
1266
1415
  return {
@@ -1311,8 +1460,24 @@ async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = ""
1311
1460
  };
1312
1461
  }
1313
1462
 
1314
- export async function startRecommendPipelineRunTool({ workspaceRoot = "", args = {} } = {}) {
1315
- const started = await startRecommendPipelineRunInternal(args, { workspaceRoot });
1463
+ export function prepareRecommendPipelineRunTool({ workspaceRoot = "", args = {} } = {}) {
1464
+ const prepared = prepareRecommendPipelineStart(args, { workspaceRoot });
1465
+ if (prepared.response) return prepared.response;
1466
+ const { parsed, normalized } = prepared;
1467
+ return {
1468
+ status: "READY",
1469
+ review: parsed.review,
1470
+ post_action: {
1471
+ requested: normalized.postAction,
1472
+ execute_post_action: args.dry_run_post_action === true ? false : args.execute_post_action !== false,
1473
+ max_greet_count: normalized.maxGreetCount
1474
+ },
1475
+ target_count_semantics: TARGET_COUNT_SEMANTICS
1476
+ };
1477
+ }
1478
+
1479
+ export async function startRecommendPipelineRunTool({ workspaceRoot = "", args = {}, runId = "" } = {}) {
1480
+ const started = await startRecommendPipelineRunInternal(args, { workspaceRoot, runId });
1316
1481
  if (started.status !== "ACCEPTED") return started;
1317
1482
  return attachMethodEvidence(started, started.run_id);
1318
1483
  }
@@ -1339,12 +1504,14 @@ export function getRecommendPipelineRunTool({ args = {} } = {}) {
1339
1504
  } catch {
1340
1505
  const persisted = readRecommendRunState(runId);
1341
1506
  if (persisted) {
1507
+ const reconciled = reconcilePersistedRecommendRunIfNeeded(persisted);
1342
1508
  return {
1343
1509
  status: "RUN_STATUS",
1344
- run: persisted,
1510
+ run: reconciled,
1345
1511
  persistence: {
1346
1512
  source: "disk",
1347
- active_control_available: false
1513
+ active_control_available: false,
1514
+ stale_process_reconciled: reconciled?.state !== persisted.state
1348
1515
  },
1349
1516
  runtime_evaluate_used: false,
1350
1517
  method_summary: {},
package/src/run-state.js CHANGED
@@ -160,6 +160,8 @@ function defaultResume(resume = {}) {
160
160
  checkpoint_path: normalizeMessage(resume?.checkpoint_path || ""),
161
161
  pause_control_path: normalizeMessage(resume?.pause_control_path || ""),
162
162
  output_csv: normalizeMessage(resume?.output_csv || ""),
163
+ worker_stdout_path: normalizeMessage(resume?.worker_stdout_path || ""),
164
+ worker_stderr_path: normalizeMessage(resume?.worker_stderr_path || ""),
163
165
  follow_up_phase: normalizeMessage(resume?.follow_up_phase || ""),
164
166
  chat_run_id: normalizeMessage(resume?.chat_run_id || ""),
165
167
  chat_state: normalizeMessage(resume?.chat_state || ""),