@reconcrap/boss-recommend-mcp 2.0.3 → 2.0.5

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.
@@ -22,6 +22,7 @@ import {
22
22
  getNextInfiniteListCandidate,
23
23
  markInfiniteListCandidateProcessed
24
24
  } from "../../core/infinite-list/index.js";
25
+ import { createViewportRunGuard } from "../../core/self-heal/index.js";
25
26
  import { createRunLifecycleManager } from "../../core/run/index.js";
26
27
  import {
27
28
  callScreeningLlm,
@@ -250,9 +251,13 @@ async function setupChatRunContext(client, {
250
251
  normalizedStartFrom,
251
252
  readyTimeoutMs,
252
253
  listSettleMs,
253
- runControl
254
+ runControl,
255
+ ensureViewport = null
254
256
  } = {}) {
255
- const rootState = await getChatRoots(client);
257
+ let rootState = await getChatRoots(client);
258
+ if (ensureViewport) {
259
+ rootState = await ensureViewport(rootState, "context_roots");
260
+ }
256
261
  runControl.checkpoint({
257
262
  top_document_node_id: rootState.rootNodes.top
258
263
  });
@@ -280,6 +285,10 @@ async function setupChatRunContext(client, {
280
285
  if (normalizeText(job) && !jobSelection.selected) {
281
286
  throw new Error(`Chat job selection failed: ${jobSelection.reason || "unknown"}`);
282
287
  }
288
+ rootState = await getChatRoots(client);
289
+ if (ensureViewport) {
290
+ rootState = await ensureViewport(rootState, "context_job");
291
+ }
283
292
  runControl.checkpoint({
284
293
  chat_context_step: "job_selection",
285
294
  primary_label: primaryLabel,
@@ -294,6 +303,10 @@ async function setupChatRunContext(client, {
294
303
  if (!startFilter.ok) {
295
304
  throw new Error(`Chat start filter selection failed: ${startFilter.error || "unknown"}`);
296
305
  }
306
+ rootState = await getChatRoots(client);
307
+ if (ensureViewport) {
308
+ rootState = await ensureViewport(rootState, "context_start_filter");
309
+ }
297
310
  runControl.checkpoint({
298
311
  chat_context_step: "start_filter",
299
312
  primary_label: primaryLabel,
@@ -362,6 +375,18 @@ export async function runChatWorkflow({
362
375
  domain: "chat",
363
376
  listName: "chat-candidates"
364
377
  });
378
+ const viewportGuard = createViewportRunGuard({
379
+ client,
380
+ domain: "chat",
381
+ root: "top",
382
+ frameOwnerRoot: "top",
383
+ runControl,
384
+ getRoots: getChatRoots
385
+ });
386
+ async function ensureChatViewport(rootState, phase) {
387
+ const result = await viewportGuard.ensure(rootState, { phase });
388
+ return result.rootState || rootState;
389
+ }
365
390
  const results = [];
366
391
  let cardNodeIds = [];
367
392
  let listEndReason = "";
@@ -398,7 +423,8 @@ export async function runChatWorkflow({
398
423
  normalizedStartFrom,
399
424
  readyTimeoutMs,
400
425
  listSettleMs,
401
- runControl
426
+ runControl,
427
+ ensureViewport: ensureChatViewport
402
428
  });
403
429
  let rootState = setup.rootState;
404
430
  contextSetup = {
@@ -431,7 +457,8 @@ export async function runChatWorkflow({
431
457
  normalizedStartFrom,
432
458
  readyTimeoutMs,
433
459
  listSettleMs,
434
- runControl
460
+ runControl,
461
+ ensureViewport: ensureChatViewport
435
462
  });
436
463
  rootState = recoveredSetup.rootState;
437
464
  contextSetup = {
@@ -449,7 +476,7 @@ export async function runChatWorkflow({
449
476
  await runControl.waitIfPaused();
450
477
  runControl.throwIfCanceled();
451
478
  runControl.setPhase("chat:cards");
452
- const cardRootState = await getChatRoots(client);
479
+ const cardRootState = await ensureChatViewport(await getChatRoots(client), "cards");
453
480
  const initialCards = await waitForChatCandidateNodeIds(client, cardRootState.rootNodes.top, {
454
481
  timeoutMs: cardTimeoutMs,
455
482
  intervalMs: 500
@@ -479,7 +506,9 @@ export async function runChatWorkflow({
479
506
  request_skipped: 0,
480
507
  unique_seen: compactInfiniteListState(listState).seen_count,
481
508
  scroll_count: compactInfiniteListState(listState).scroll_count,
482
- list_end_reason: listEndReason
509
+ list_end_reason: listEndReason,
510
+ viewport_checks: viewportGuard.getStats().checks,
511
+ viewport_recoveries: viewportGuard.getStats().recoveries
483
512
  });
484
513
  runControl.setPhase("chat:done");
485
514
  return {
@@ -493,6 +522,10 @@ export async function runChatWorkflow({
493
522
  requested_start_from: normalizedStartFrom
494
523
  },
495
524
  candidate_list: compactInfiniteListState(listState),
525
+ viewport_health: {
526
+ stats: viewportGuard.getStats(),
527
+ events: viewportGuard.getEvents()
528
+ },
496
529
  list_end_reason: listEndReason,
497
530
  target_pass_count: passTarget,
498
531
  process_until_list_end: Boolean(processUntilListEnd),
@@ -524,7 +557,9 @@ export async function runChatWorkflow({
524
557
  request_satisfied: 0,
525
558
  request_skipped: 0,
526
559
  unique_seen: compactInfiniteListState(listState).seen_count,
527
- scroll_count: 0
560
+ scroll_count: 0,
561
+ viewport_checks: viewportGuard.getStats().checks,
562
+ viewport_recoveries: viewportGuard.getStats().recoveries
528
563
  });
529
564
 
530
565
  while (
@@ -537,6 +572,7 @@ export async function runChatWorkflow({
537
572
  await runControl.waitIfPaused();
538
573
  runControl.throwIfCanceled();
539
574
  runControl.setPhase("chat:candidate");
575
+ rootState = await ensureChatViewport(rootState, "candidate_loop");
540
576
  const loopTopLevelState = await getChatTopLevelState(client);
541
577
  if (!loopTopLevelState.is_chat_shell) {
542
578
  await recoverAndReapplyChatContext("candidate_loop_non_chat_shell", {
@@ -554,7 +590,8 @@ export async function runChatWorkflow({
554
590
  settleMs: listSettleMs,
555
591
  fallbackPoint: listFallbackPoint,
556
592
  findNodeIds: async () => {
557
- const currentRootState = await getChatRoots(client);
593
+ const currentRootState = await ensureChatViewport(await getChatRoots(client), "candidate_find_nodes");
594
+ rootState = currentRootState;
558
595
  const currentCards = await waitForChatCandidateNodeIds(client, currentRootState.rootNodes.top, {
559
596
  timeoutMs: Math.min(cardTimeoutMs, 8000),
560
597
  intervalMs: 500
@@ -609,6 +646,7 @@ export async function runChatWorkflow({
609
646
  await runControl.waitIfPaused();
610
647
  runControl.throwIfCanceled();
611
648
  runControl.setPhase("chat:detail");
649
+ rootState = await ensureChatViewport(rootState, "detail");
612
650
 
613
651
  detailStep = "select_candidate";
614
652
  networkRecorder.clear();
@@ -921,6 +959,8 @@ export async function runChatWorkflow({
921
959
  unique_seen: compactInfiniteListState(listState).seen_count,
922
960
  scroll_count: compactInfiniteListState(listState).scroll_count,
923
961
  list_end_reason: listEndReason || null,
962
+ viewport_checks: viewportGuard.getStats().checks,
963
+ viewport_recoveries: viewportGuard.getStats().recoveries,
924
964
  last_candidate_id: screeningCandidate.id || null,
925
965
  last_candidate_key: candidateKey,
926
966
  last_score: screening.score
@@ -950,6 +990,10 @@ export async function runChatWorkflow({
950
990
  card_count: cardNodeIds.length,
951
991
  context_setup: contextSetup,
952
992
  candidate_list: compactInfiniteListState(listState),
993
+ viewport_health: {
994
+ stats: viewportGuard.getStats(),
995
+ events: viewportGuard.getEvents()
996
+ },
953
997
  list_end_reason: listEndReason || null,
954
998
  target_pass_count: passTarget,
955
999
  process_until_list_end: Boolean(processUntilListEnd),
@@ -25,7 +25,8 @@ export async function getRecommendRoots(client, {
25
25
  ].filter(Boolean),
26
26
  rootNodes: {
27
27
  top: topRoot.nodeId,
28
- frame: iframe?.documentNodeId || 0
28
+ frame: iframe?.documentNodeId || 0,
29
+ frameOwner: iframe?.nodeId || 0
29
30
  }
30
31
  };
31
32
  }
@@ -49,7 +50,8 @@ export async function waitForRecommendRoots(client, {
49
50
  roots: [],
50
51
  rootNodes: {
51
52
  top: 0,
52
- frame: 0
53
+ frame: 0,
54
+ frameOwner: 0
53
55
  }
54
56
  };
55
57
  }
@@ -20,6 +20,7 @@ import {
20
20
  markInfiniteListCandidateProcessed,
21
21
  resetInfiniteListForRefreshRound
22
22
  } from "../../core/infinite-list/index.js";
23
+ import { createViewportRunGuard } from "../../core/self-heal/index.js";
23
24
  import { screenCandidate } from "../../core/screening/index.js";
24
25
  import {
25
26
  closeRecommendDetail,
@@ -372,6 +373,18 @@ export async function runRecommendWorkflow({
372
373
  domain: "recommend",
373
374
  listName: "recommend-candidates"
374
375
  });
376
+ const viewportGuard = createViewportRunGuard({
377
+ client,
378
+ domain: "recommend",
379
+ root: "frame",
380
+ frameOwnerRoot: "frameOwner",
381
+ runControl,
382
+ getRoots: getRecommendRoots
383
+ });
384
+ async function ensureRecommendViewport(rootState, phase) {
385
+ const result = await viewportGuard.ensure(rootState, { phase });
386
+ return result.rootState || rootState;
387
+ }
375
388
  const results = [];
376
389
  const refreshAttempts = [];
377
390
  let refreshRounds = 0;
@@ -389,6 +402,7 @@ export async function runRecommendWorkflow({
389
402
  runControl.throwIfCanceled();
390
403
  runControl.setPhase("recommend:roots");
391
404
  let rootState = await getRecommendRoots(client);
405
+ rootState = await ensureRecommendViewport(rootState, "roots");
392
406
  runControl.checkpoint({
393
407
  iframe_selector: rootState.iframe.selector,
394
408
  iframe_document_node_id: rootState.iframe.documentNodeId
@@ -406,6 +420,7 @@ export async function runRecommendWorkflow({
406
420
  throw new Error(`Requested recommend job was not selected: ${jobSelection.reason}`);
407
421
  }
408
422
  rootState = await getRecommendRoots(client);
423
+ rootState = await ensureRecommendViewport(rootState, "job");
409
424
  runControl.checkpoint({
410
425
  job_selection: compactJobSelection(jobSelection)
411
426
  });
@@ -424,6 +439,7 @@ export async function runRecommendWorkflow({
424
439
  throw new Error(`Recommend page scope was not selected: ${pageScopeSelection.reason || pageScopeSelection.effective_scope || requestedPageScope}`);
425
440
  }
426
441
  rootState = await getRecommendRoots(client);
442
+ rootState = await ensureRecommendViewport(rootState, "page_scope");
427
443
  runControl.checkpoint({
428
444
  page_scope: compactPageScopeSelection(pageScopeSelection)
429
445
  });
@@ -440,6 +456,8 @@ export async function runRecommendWorkflow({
440
456
  if (!filterResult.confirmed) {
441
457
  throw new Error("Recommend run filter selection was not confirmed");
442
458
  }
459
+ rootState = await getRecommendRoots(client);
460
+ rootState = await ensureRecommendViewport(rootState, "filter");
443
461
  runControl.checkpoint({
444
462
  filter: compactFilterResult(filterResult)
445
463
  });
@@ -448,6 +466,7 @@ export async function runRecommendWorkflow({
448
466
  await runControl.waitIfPaused();
449
467
  runControl.throwIfCanceled();
450
468
  runControl.setPhase("recommend:cards");
469
+ rootState = await ensureRecommendViewport(rootState, "cards");
451
470
  cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
452
471
  timeoutMs: cardTimeoutMs,
453
472
  intervalMs: 300
@@ -468,13 +487,16 @@ export async function runRecommendWorkflow({
468
487
  unique_seen: compactInfiniteListState(listState).seen_count,
469
488
  scroll_count: 0,
470
489
  refresh_rounds: 0,
471
- refresh_attempts: 0
490
+ refresh_attempts: 0,
491
+ viewport_checks: viewportGuard.getStats().checks,
492
+ viewport_recoveries: viewportGuard.getStats().recoveries
472
493
  });
473
494
 
474
495
  while (results.length < limit) {
475
496
  await runControl.waitIfPaused();
476
497
  runControl.throwIfCanceled();
477
498
  runControl.setPhase("recommend:candidate");
499
+ rootState = await ensureRecommendViewport(rootState, "candidate_loop");
478
500
 
479
501
  const nextCandidateResult = await getNextInfiniteListCandidate({
480
502
  client,
@@ -485,7 +507,9 @@ export async function runRecommendWorkflow({
485
507
  settleMs: listSettleMs,
486
508
  fallbackPoint: listFallbackPoint,
487
509
  findNodeIds: async () => {
488
- const currentRootState = await getRecommendRoots(client);
510
+ let currentRootState = await getRecommendRoots(client);
511
+ currentRootState = await ensureRecommendViewport(currentRootState, "candidate_find_nodes");
512
+ rootState = currentRootState;
489
513
  const currentCardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
490
514
  timeoutMs: Math.min(cardTimeoutMs, 5000),
491
515
  intervalMs: 300
@@ -544,10 +568,13 @@ export async function runRecommendWorkflow({
544
568
  refresh_attempts: refreshAttempts.length,
545
569
  refresh_method: refreshResult.method || null,
546
570
  refresh_forced_recent_not_view: true,
547
- list_end_reason: listEndReason
571
+ list_end_reason: listEndReason,
572
+ viewport_checks: viewportGuard.getStats().checks,
573
+ viewport_recoveries: viewportGuard.getStats().recoveries
548
574
  });
549
575
  if (refreshResult.ok) {
550
576
  rootState = refreshResult.root_state || await getRecommendRoots(client);
577
+ rootState = await ensureRecommendViewport(rootState, "refresh_after");
551
578
  cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
552
579
  timeoutMs: cardTimeoutMs,
553
580
  intervalMs: 300
@@ -579,6 +606,7 @@ export async function runRecommendWorkflow({
579
606
  await runControl.waitIfPaused();
580
607
  runControl.throwIfCanceled();
581
608
  runControl.setPhase("recommend:detail");
609
+ rootState = await ensureRecommendViewport(rootState, "detail");
582
610
  networkRecorder.clear();
583
611
  const openedDetail = await openRecommendCardDetail(client, cardNodeId);
584
612
  const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
@@ -719,6 +747,8 @@ export async function runRecommendWorkflow({
719
747
  refresh_rounds: refreshRounds,
720
748
  refresh_attempts: refreshAttempts.length,
721
749
  list_end_reason: listEndReason || null,
750
+ viewport_checks: viewportGuard.getStats().checks,
751
+ viewport_recoveries: viewportGuard.getStats().recoveries,
722
752
  last_candidate_id: screeningCandidate.id || null,
723
753
  last_candidate_key: candidateKey,
724
754
  last_score: screening.score
@@ -756,6 +786,10 @@ export async function runRecommendWorkflow({
756
786
  filter: compactFilterResult(filterResult),
757
787
  card_count: cardNodeIds.length,
758
788
  candidate_list: compactInfiniteListState(listState),
789
+ viewport_health: {
790
+ stats: viewportGuard.getStats(),
791
+ events: viewportGuard.getEvents()
792
+ },
759
793
  list_end_reason: listEndReason || null,
760
794
  refresh_rounds: refreshRounds,
761
795
  refresh_attempts: refreshAttempts,
@@ -25,7 +25,8 @@ export async function getRecruitRoots(client, {
25
25
  ].filter(Boolean),
26
26
  rootNodes: {
27
27
  top: topRoot.nodeId,
28
- frame: iframe?.documentNodeId || 0
28
+ frame: iframe?.documentNodeId || 0,
29
+ frameOwner: iframe?.nodeId || 0
29
30
  }
30
31
  };
31
32
  }
@@ -18,6 +18,7 @@ import {
18
18
  markInfiniteListCandidateProcessed,
19
19
  resetInfiniteListForRefreshRound
20
20
  } from "../../core/infinite-list/index.js";
21
+ import { createViewportRunGuard } from "../../core/self-heal/index.js";
21
22
  import { screenCandidate } from "../../core/screening/index.js";
22
23
  import {
23
24
  closeRecruitDetail,
@@ -140,6 +141,18 @@ export async function runRecruitWorkflow({
140
141
  domain: "recruit",
141
142
  listName: "search-results"
142
143
  });
144
+ const viewportGuard = createViewportRunGuard({
145
+ client,
146
+ domain: "recruit",
147
+ root: "frame",
148
+ frameOwnerRoot: "frameOwner",
149
+ runControl,
150
+ getRoots: getRecruitRoots
151
+ });
152
+ async function ensureRecruitViewport(rootState, phase) {
153
+ const result = await viewportGuard.ensure(rootState, { phase });
154
+ return result.rootState || rootState;
155
+ }
143
156
  const results = [];
144
157
  const refreshAttempts = [];
145
158
  let refreshRounds = 0;
@@ -153,6 +166,7 @@ export async function runRecruitWorkflow({
153
166
  runControl.throwIfCanceled();
154
167
  runControl.setPhase("recruit:roots");
155
168
  let rootState = await getRecruitRoots(client);
169
+ rootState = await ensureRecruitViewport(rootState, "roots");
156
170
  runControl.checkpoint({
157
171
  iframe_selector: rootState.iframe.selector,
158
172
  iframe_document_node_id: rootState.iframe.documentNodeId,
@@ -186,11 +200,13 @@ export async function runRecruitWorkflow({
186
200
  }
187
201
  });
188
202
  rootState = await getRecruitRoots(client);
203
+ rootState = await ensureRecruitViewport(rootState, "search");
189
204
  }
190
205
 
191
206
  await runControl.waitIfPaused();
192
207
  runControl.throwIfCanceled();
193
208
  runControl.setPhase("recruit:cards");
209
+ rootState = await ensureRecruitViewport(rootState, "cards");
194
210
  cardNodeIds = await waitForRecruitCardNodeIds(client, rootState.iframe.documentNodeId, {
195
211
  timeoutMs: cardTimeoutMs,
196
212
  intervalMs: 300
@@ -209,13 +225,16 @@ export async function runRecruitWorkflow({
209
225
  unique_seen: compactInfiniteListState(listState).seen_count,
210
226
  scroll_count: 0,
211
227
  refresh_rounds: 0,
212
- refresh_attempts: 0
228
+ refresh_attempts: 0,
229
+ viewport_checks: viewportGuard.getStats().checks,
230
+ viewport_recoveries: viewportGuard.getStats().recoveries
213
231
  });
214
232
 
215
233
  while (results.length < limit) {
216
234
  await runControl.waitIfPaused();
217
235
  runControl.throwIfCanceled();
218
236
  runControl.setPhase("recruit:candidate");
237
+ rootState = await ensureRecruitViewport(rootState, "candidate_loop");
219
238
 
220
239
  const nextCandidateResult = await getNextInfiniteListCandidate({
221
240
  client,
@@ -226,7 +245,9 @@ export async function runRecruitWorkflow({
226
245
  settleMs: listSettleMs,
227
246
  fallbackPoint: listFallbackPoint,
228
247
  findNodeIds: async () => {
229
- const currentRootState = await getRecruitRoots(client);
248
+ let currentRootState = await getRecruitRoots(client);
249
+ currentRootState = await ensureRecruitViewport(currentRootState, "candidate_find_nodes");
250
+ rootState = currentRootState;
230
251
  const currentCardNodeIds = await waitForRecruitCardNodeIds(client, currentRootState.iframe.documentNodeId, {
231
252
  timeoutMs: Math.min(cardTimeoutMs, 5000),
232
253
  intervalMs: 300
@@ -283,10 +304,13 @@ export async function runRecruitWorkflow({
283
304
  refresh_attempts: refreshAttempts.length,
284
305
  refresh_method: refreshResult.method || null,
285
306
  refresh_forced_recent_viewed: true,
286
- list_end_reason: listEndReason
307
+ list_end_reason: listEndReason,
308
+ viewport_checks: viewportGuard.getStats().checks,
309
+ viewport_recoveries: viewportGuard.getStats().recoveries
287
310
  });
288
311
  if (refreshResult.ok) {
289
312
  rootState = await getRecruitRoots(client);
313
+ rootState = await ensureRecruitViewport(rootState, "refresh_after");
290
314
  cardNodeIds = await waitForRecruitCardNodeIds(client, rootState.iframe.documentNodeId, {
291
315
  timeoutMs: cardTimeoutMs,
292
316
  intervalMs: 300
@@ -318,6 +342,7 @@ export async function runRecruitWorkflow({
318
342
  await runControl.waitIfPaused();
319
343
  runControl.throwIfCanceled();
320
344
  runControl.setPhase("recruit:detail");
345
+ rootState = await ensureRecruitViewport(rootState, "detail");
321
346
  networkRecorder.clear();
322
347
  const openedDetail = await openRecruitCardDetail(client, cardNodeId);
323
348
  const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
@@ -430,6 +455,8 @@ export async function runRecruitWorkflow({
430
455
  refresh_rounds: refreshRounds,
431
456
  refresh_attempts: refreshAttempts.length,
432
457
  list_end_reason: listEndReason || null,
458
+ viewport_checks: viewportGuard.getStats().checks,
459
+ viewport_recoveries: viewportGuard.getStats().recoveries,
433
460
  last_candidate_id: screeningCandidate.id || null,
434
461
  last_candidate_key: candidateKey,
435
462
  last_score: screening.score
@@ -459,6 +486,10 @@ export async function runRecruitWorkflow({
459
486
  search_params: normalizedSearchParams,
460
487
  card_count: cardNodeIds.length,
461
488
  candidate_list: compactInfiniteListState(listState),
489
+ viewport_health: {
490
+ stats: viewportGuard.getStats(),
491
+ events: viewportGuard.getEvents()
492
+ },
462
493
  list_end_reason: listEndReason || null,
463
494
  refresh_rounds: refreshRounds,
464
495
  refresh_attempts: refreshAttempts,
package/src/index.js CHANGED
@@ -7,7 +7,8 @@ import { fileURLToPath } from "node:url";
7
7
  import {
8
8
  getFeaturedCalibrationResolution,
9
9
  getBossChatTargetCountValue,
10
- normalizeTargetCountInput
10
+ normalizeTargetCountInput,
11
+ resolveBossScreeningConfig
11
12
  } from "./chat-runtime-config.js";
12
13
  import {
13
14
  __resetChatMcpStateForTests,
@@ -1890,7 +1891,8 @@ async function handleRunRecommendSelfHealTool({ workspaceRoot, args }) {
1890
1891
  }
1891
1892
 
1892
1893
  const host = "127.0.0.1";
1893
- const port = parsePositiveInteger(args.port, 9222);
1894
+ const configResolution = resolveBossScreeningConfig(workspaceRoot);
1895
+ const port = parsePositiveInteger(args.port, configResolution.ok ? configResolution.config.debugPort : 9222);
1894
1896
  let session = null;
1895
1897
  try {
1896
1898
  session = await connectToChromeTarget({
@@ -1909,7 +1911,8 @@ async function handleRunRecommendSelfHealTool({ workspaceRoot, args }) {
1909
1911
  domain: "recommend",
1910
1912
  roots: rootState?.roots || {},
1911
1913
  selectorProbes: config.selectorProbes,
1912
- accessibilityProbes: config.accessibilityProbes
1914
+ accessibilityProbes: config.accessibilityProbes,
1915
+ viewportProbes: config.viewportProbes
1913
1916
  });
1914
1917
  assertNoForbiddenCdpCalls(methodLog);
1915
1918
 
@@ -44,6 +44,10 @@ import {
44
44
  parseRecommendInstruction
45
45
  } from "./parser.js";
46
46
  import { getRunsDir } from "./run-state.js";
47
+ import {
48
+ resolveBossConfiguredOutputDir,
49
+ resolveBossScreeningConfig
50
+ } from "./chat-runtime-config.js";
47
51
 
48
52
  const DEFAULT_RECOMMEND_HOST = "127.0.0.1";
49
53
  const DEFAULT_RECOMMEND_PORT = 9222;
@@ -115,12 +119,14 @@ function getRecommendRunArtifacts(runId) {
115
119
  const normalized = normalizeRunId(runId);
116
120
  if (!normalized) return null;
117
121
  const runsDir = getRunsDir();
122
+ const outputDir = resolveBossConfiguredOutputDir("", runsDir);
118
123
  return {
119
124
  runs_dir: runsDir,
125
+ output_dir: outputDir,
120
126
  run_state_path: path.join(runsDir, `${normalized}.json`),
121
127
  checkpoint_path: path.join(runsDir, `${normalized}.checkpoint.json`),
122
- output_csv: path.join(runsDir, `${normalized}.results.csv`),
123
- report_json: path.join(runsDir, `${normalized}.report.json`)
128
+ output_csv: path.join(outputDir, `${normalized}.results.csv`),
129
+ report_json: path.join(outputDir, `${normalized}.report.json`)
124
130
  };
125
131
  }
126
132
 
@@ -484,8 +490,12 @@ async function readRecommendJobOptionsFromSession(session) {
484
490
  }
485
491
 
486
492
  export async function listRecommendJobsTool({ workspaceRoot = "", args = {} } = {}) {
493
+ const configResolution = resolveBossScreeningConfig(workspaceRoot);
487
494
  const host = normalizeText(args.host) || DEFAULT_RECOMMEND_HOST;
488
- const port = parsePositiveInteger(args.port, DEFAULT_RECOMMEND_PORT);
495
+ const port = parsePositiveInteger(
496
+ args.port,
497
+ configResolution.ok ? configResolution.config.debugPort : DEFAULT_RECOMMEND_PORT
498
+ );
489
499
  const targetUrlIncludes = normalizeText(args.target_url_includes) || RECOMMEND_TARGET_URL;
490
500
  const allowNavigate = args.allow_navigate !== false;
491
501
  const slowLive = args.slow_live === true;
@@ -613,7 +623,8 @@ async function waitForHealthyRecommend(client, config, {
613
623
  domain: "recommend",
614
624
  roots: roots.roots,
615
625
  selectorProbes: config.selectorProbes,
616
- accessibilityProbes: config.accessibilityProbes
626
+ accessibilityProbes: config.accessibilityProbes,
627
+ viewportProbes: config.viewportProbes
617
628
  });
618
629
  if (lastCheck.status === HEALTH_STATUS.HEALTHY) return lastCheck;
619
630
  await sleep(intervalMs);
@@ -922,7 +933,7 @@ function buildRecommendFilter(parsed, args = {}) {
922
933
  return groups.length ? { filterGroups: groups } : { enabled: false };
923
934
  }
924
935
 
925
- function normalizeRecommendStartInput(args = {}, parsed) {
936
+ function normalizeRecommendStartInput(args = {}, parsed, configResolution = null) {
926
937
  const confirmation = args.confirmation || {};
927
938
  const overrides = args.overrides || {};
928
939
  const slowLive = args.slow_live === true;
@@ -932,7 +943,10 @@ function normalizeRecommendStartInput(args = {}, parsed) {
932
943
  );
933
944
  return {
934
945
  host: normalizeText(args.host) || DEFAULT_RECOMMEND_HOST,
935
- port: parsePositiveInteger(args.port, DEFAULT_RECOMMEND_PORT),
946
+ port: parsePositiveInteger(
947
+ args.port,
948
+ configResolution?.ok ? configResolution.config.debugPort : DEFAULT_RECOMMEND_PORT
949
+ ),
936
950
  targetUrlIncludes: normalizeText(args.target_url_includes) || RECOMMEND_TARGET_URL,
937
951
  allowNavigate: args.allow_navigate !== false,
938
952
  slowLive,
@@ -1038,7 +1052,8 @@ async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = ""
1038
1052
  const parsed = parseRecommendPipelineRequest(args);
1039
1053
  const gate = evaluateRecommendPipelineGate(parsed, args);
1040
1054
  if (gate) return gate;
1041
- const normalized = normalizeRecommendStartInput(args, parsed);
1055
+ const configResolution = resolveBossScreeningConfig(workspaceRoot);
1056
+ const normalized = normalizeRecommendStartInput(args, parsed, configResolution);
1042
1057
 
1043
1058
  let session;
1044
1059
  try {
@@ -32,6 +32,10 @@ import {
32
32
  runRecruitWorkflow,
33
33
  waitForRecruitSearchControls
34
34
  } from "./domains/recruit/index.js";
35
+ import {
36
+ resolveBossConfiguredOutputDir,
37
+ resolveBossScreeningConfig
38
+ } from "./chat-runtime-config.js";
35
39
 
36
40
  const RUN_MODE_ASYNC = "async";
37
41
  const RUN_MODE_SYNC = "sync";
@@ -108,12 +112,14 @@ function getRecruitRunArtifacts(runId) {
108
112
  const normalized = normalizeRunId(runId);
109
113
  if (!normalized) return null;
110
114
  const runsDir = getRecruitRunsDir();
115
+ const outputDir = resolveBossConfiguredOutputDir("", runsDir);
111
116
  return {
112
117
  runs_dir: runsDir,
118
+ output_dir: outputDir,
113
119
  run_state_path: path.join(runsDir, `${normalized}.json`),
114
120
  checkpoint_path: path.join(runsDir, `${normalized}.checkpoint.json`),
115
- output_csv: path.join(runsDir, `${normalized}.results.csv`),
116
- report_json: path.join(runsDir, `${normalized}.report.json`)
121
+ output_csv: path.join(outputDir, `${normalized}.results.csv`),
122
+ report_json: path.join(outputDir, `${normalized}.report.json`)
117
123
  };
118
124
  }
119
125
 
@@ -807,12 +813,18 @@ async function startRecruitPipelineRunInternal(args = {}, { workspaceRoot = "" }
807
813
  const parsed = parseRecruitPipelineRequest(args);
808
814
  const gate = evaluateRecruitPipelineGate(parsed);
809
815
  if (gate) return gate;
816
+ const configResolution = resolveBossScreeningConfig(workspaceRoot);
817
+ const host = normalizeText(args.host) || DEFAULT_RECRUIT_HOST;
818
+ const port = parsePositiveInteger(
819
+ args.port,
820
+ configResolution.ok ? configResolution.config.debugPort : DEFAULT_RECRUIT_PORT
821
+ );
810
822
 
811
823
  let session;
812
824
  try {
813
825
  session = await recruitConnectorImpl({
814
- host: normalizeText(args.host) || DEFAULT_RECRUIT_HOST,
815
- port: parsePositiveInteger(args.port, DEFAULT_RECRUIT_PORT),
826
+ host,
827
+ port,
816
828
  targetUrlIncludes: normalizeText(args.target_url_includes) || RECRUIT_TARGET_URL,
817
829
  allowNavigate: args.allow_navigate !== false,
818
830
  slowLive: args.slow_live === true
@@ -858,8 +870,8 @@ async function startRecruitPipelineRunInternal(args = {}, { workspaceRoot = "" }
858
870
  workspaceRoot: normalizeText(workspaceRoot) || globalThis.process?.cwd?.() || "",
859
871
  args: clonePlain(args, {}),
860
872
  chrome: {
861
- host: normalizeText(args.host) || DEFAULT_RECRUIT_HOST,
862
- port: parsePositiveInteger(args.port, DEFAULT_RECRUIT_PORT),
873
+ host,
874
+ port,
863
875
  target_url: session.target?.url || RECRUIT_TARGET_URL,
864
876
  target_id: session.target?.id || null,
865
877
  auto_launch: session.chrome || null