@bbradar/mcp 0.3.0 → 0.3.1
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/CHANGELOG.md +10 -0
- package/README.md +42 -0
- package/benchmark/live-api-check.mjs +126 -0
- package/benchmark/live-hunt.mjs +105 -0
- package/benchmark/live-stdio.mjs +80 -0
- package/dist/activityContract.d.ts +2 -0
- package/dist/activityContract.js +32 -0
- package/dist/activityContract.js.map +1 -1
- package/dist/bbradarClient.d.ts +5 -1
- package/dist/bbradarClient.js +24 -3
- package/dist/bbradarClient.js.map +1 -1
- package/dist/server.js +212 -94
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { requiresActivityQuery } from "./activityContract.js";
|
|
2
|
+
import { requiresActivityQuery, validateActivityCatalogInput } from "./activityContract.js";
|
|
3
3
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { BBRadarApiError } from "./bbradarClient.js";
|
|
@@ -7,7 +7,7 @@ import { NumericCursorStore } from "./cursorStore.js";
|
|
|
7
7
|
import { stableStringify, stripUndefined } from "./json.js";
|
|
8
8
|
import { FixedWindowRateLimiter } from "./rateLimit.js";
|
|
9
9
|
import { readArray, readBoolean, readNumber, readObject, readString, sanitizeJson, sanitizeProPayload, sanitizeChange, sanitizeProgram, sanitizeRepositoryChange, sanitizeTarget, sanitizeTargetsForExport } from "./sanitize.js";
|
|
10
|
-
const SERVER_VERSION = "0.3.
|
|
10
|
+
const SERVER_VERSION = "0.3.1";
|
|
11
11
|
const MAX_PROGRAMS_PER_SEARCH = 100;
|
|
12
12
|
const MAX_TARGET_EXPORT = 500;
|
|
13
13
|
const MAX_RECENT_CHANGES = 100;
|
|
@@ -46,7 +46,7 @@ const MAX_IN_MEMORY_FEED_CURSORS = 500;
|
|
|
46
46
|
const MAX_RESOLVE_SEARCH_QUERIES = 4;
|
|
47
47
|
const FAST_RESOLVE_SEARCH_QUERIES = 2;
|
|
48
48
|
const MAX_DIRECT_HANDLE_RESOLUTION_REQUESTS = 96;
|
|
49
|
-
const SDK_VERSION = "1.
|
|
49
|
+
const SDK_VERSION = "1.31.0";
|
|
50
50
|
const CORE_TOOL_NAMES = new Set([
|
|
51
51
|
"search_programs",
|
|
52
52
|
"get_program",
|
|
@@ -257,11 +257,11 @@ const stableSnapshotSchema = z
|
|
|
257
257
|
.string()
|
|
258
258
|
.trim()
|
|
259
259
|
.min(1)
|
|
260
|
-
.max(
|
|
260
|
+
.max(32, "Catalog snapshot tokens have at most 32 characters. Use 'start', then meta.snapshot; Activity snapshot IDs belong only to Activity and payout tools.")
|
|
261
261
|
.refine((value) => !/[\u0000-\u001F\u007F]/.test(value), {
|
|
262
262
|
message: "Snapshot tokens cannot contain control characters."
|
|
263
263
|
})
|
|
264
|
-
.describe("Optional
|
|
264
|
+
.describe("Optional catalog/feed snapshot token, at most 32 characters. Send 'start' on page 1, then reuse meta.snapshot with identical filters. Do not pass get_activity_snapshot's snapshot_id here. Snapshot creation can be slower; omit it for the fastest live result.");
|
|
265
265
|
const opaqueCursorSchema = z.string().min(1).max(4096).describe("Opaque five-minute cursor. Reuse with identical filters and page size; restart the query on expiry.");
|
|
266
266
|
const analyticsDateSchema = () => z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Use an inclusive UTC YYYY-MM-DD date.");
|
|
267
267
|
const proLimitSchema = z.number().int().min(1).max(100);
|
|
@@ -309,7 +309,7 @@ const activityEventPeriodSchema = () => z.enum(["7d", "30d", "90d"]);
|
|
|
309
309
|
const activityCatalogNumberSchema = () => z.number().finite().min(0).optional();
|
|
310
310
|
const activityCatalogInput = {
|
|
311
311
|
program_type: z.enum(["paid", "vdp", "unknown"]).optional(),
|
|
312
|
-
payout_metric: z.enum(["payout_total", "payout_increase", "payout_native_90d"]).optional(),
|
|
312
|
+
payout_metric: z.enum(["payout_total", "payout_increase", "payout_native_90d"]).optional().describe("payout_total requires all_time; payout_increase is a tracked counter increase in 24h/7d/30d/90d; payout_native_90d requires 90d and is provider-reported. Omit to select the period's default metric."),
|
|
313
313
|
payout_period: activityCountPeriodSchema().optional(),
|
|
314
314
|
payout_precision: z.enum(["exact", "proven_bounds"]).optional(),
|
|
315
315
|
payout_min: activityCatalogNumberSchema(), payout_max: activityCatalogNumberSchema(),
|
|
@@ -322,7 +322,7 @@ const activityCatalogInput = {
|
|
|
322
322
|
reward_change: z.enum(["increased", "decreased"]).optional(), reward_change_min_pct: activityCatalogNumberSchema(),
|
|
323
323
|
updates_period: activityEventPeriodSchema().optional(),
|
|
324
324
|
active_days_min: activityCatalogNumberSchema(), changes_min: activityCatalogNumberSchema(),
|
|
325
|
-
change_category: z.enum(["scope", "rewards", "rules", "program"]).optional(),
|
|
325
|
+
change_category: z.enum(["scope", "rewards", "rules", "program"]).optional().describe("Update-event category; requires its own updates_period. scope_period with scope_added_min already filters scope additions and does not need this field."),
|
|
326
326
|
reports_period: activityCountPeriodSchema().optional(),
|
|
327
327
|
reports_min: activityCatalogNumberSchema(), reports_max: activityCatalogNumberSchema(),
|
|
328
328
|
reports_change: z.enum(["rising", "falling"]).optional(), reports_change_min_pct: activityCatalogNumberSchema(),
|
|
@@ -336,7 +336,7 @@ const activityCatalogInput = {
|
|
|
336
336
|
activity_labels: z.array(z.string().min(1).max(64)).max(10).optional(),
|
|
337
337
|
sort_by: activityCatalogSortSchema.optional(), sort_direction: z.enum(["asc", "desc"]).optional()
|
|
338
338
|
};
|
|
339
|
-
const activityCatalogGuidance = "Give each Activity signal its period
|
|
339
|
+
const activityCatalogGuidance = "Give each Activity signal its own period. payout_total requires all_time; payout_increase uses 24h/7d/30d/90d; payout_native_90d requires 90d. change_category requires updates_period even when scope_period is present; scope_period with scope_added_min needs no change_category. Use enabled sort_by/sort_direction combinations; default is updated descending. Tracked no_increase/lower require 30d. Catalog snapshot is 'start' or meta.snapshot, never an Activity snapshot_id. An empty result does not authorize relaxing the user's thresholds or restricting platforms to a recent-feed sample.";
|
|
340
340
|
const freeSubmissionOnlySchema = z.boolean().default(false).describe("Only programs with no detected submission fee or deposit.");
|
|
341
341
|
const programIdSchema = z
|
|
342
342
|
.string()
|
|
@@ -444,7 +444,13 @@ function createProfiledToolRegistrar(server, profile, client) {
|
|
|
444
444
|
const extra = args[args.length - 1];
|
|
445
445
|
return client.withRequestContext(extra.signal, () => callback(...args), readObject(args[0])?.snapshot === "start");
|
|
446
446
|
});
|
|
447
|
-
|
|
447
|
+
// Raw SDK shapes otherwise strip unknown arguments. Every advertised
|
|
448
|
+
// filter must be applied or rejected, including on the older shortcuts.
|
|
449
|
+
const inputSchema = toolConfig.inputSchema;
|
|
450
|
+
const strictConfig = inputSchema && !(inputSchema instanceof z.ZodType)
|
|
451
|
+
? { ...toolConfig, inputSchema: z.object(inputSchema).strict() }
|
|
452
|
+
: toolConfig;
|
|
453
|
+
const registeredTool = server.registerTool(name, strictConfig, scopedCallback);
|
|
448
454
|
const requiredEndpoint = TOOL_ENDPOINT_REQUIREMENTS[name];
|
|
449
455
|
if ((profile === "core" && !CORE_TOOL_NAMES.has(name)) ||
|
|
450
456
|
(requiredEndpoint !== undefined && !client.supportsEndpoint(requiredEndpoint))) {
|
|
@@ -462,7 +468,8 @@ export function createBbradarServer(client, config) {
|
|
|
462
468
|
"Use find_hunt_targets once for requests combining good rewards, broad scope, target quality, and recent target additions. Use find_repository_hunt_targets instead when Git or repository activity is explicitly required.",
|
|
463
469
|
"Use find_programs for lower-level program-only discovery and find_programs_by_target for reverse target lookup.",
|
|
464
470
|
"For payouts, reports, response measures or Activity comparisons, first read get_activity_capabilities, then get_activity_rankings with the matching tab for one ranking. For several conditions at once (e.g. recent payouts plus new scope, Web2-only, low competition, no submission fee) use one find_hunt_targets or find_programs call with Activity inputs and local filters together. Keep periods explicit; never intersect separately paginated rankings.",
|
|
465
|
-
"Activity data is published in versions. get_activity_snapshot
|
|
471
|
+
"Activity data is published in versions. get_activity_snapshot's snapshot_id binds only Activity capabilities, Activity rankings and program payouts. Catalog/feed tools use snapshot=start then meta.snapshot instead; do not send an Activity snapshot_id to find_programs. Never mix rows from different versions.",
|
|
472
|
+
"Preserve the user's thresholds, periods and platform scope when discovery times out or returns no matches. A sampled recent-change feed does not enumerate all supported platforms. Inspect partial_success, errors, ranking_scope and scan before claiming complete coverage.",
|
|
466
473
|
"For semantic changes use get_history_changes or get_program_history, then get_history_change_detail only when before/after items are needed. For exact activity use get_program_activity; for ranked activity use get_analytics_program_activity. Use daily analytics tools directly for date series.",
|
|
467
474
|
"Default to bounty-eligible in-scope data unless the user explicitly requests broader results.",
|
|
468
475
|
"Prefer compact/identifier outputs and disclose sampled ranking scope when present."
|
|
@@ -474,7 +481,8 @@ export function createBbradarServer(client, config) {
|
|
|
474
481
|
"Do not repeat program discovery with alternate sorts or fan out get_program_targets across a shortlist; find_hunt_targets already includes exact bounded samples and recent target additions.",
|
|
475
482
|
"Use advanced tools only after the user's goal is clear or the guide points to them.",
|
|
476
483
|
"For payouts, reports, response measures or Activity comparisons, first read get_activity_capabilities. Use get_activity_rankings (or its per-view alias) for one ranking, or one find_hunt_targets/find_programs call combining Activity inputs with Web2, scope, competition and fee filters. Keep server order and explicit periods; never re-rank a sampled catalog or intersect separate pages.",
|
|
477
|
-
"Activity data is published in versions. get_activity_snapshot
|
|
484
|
+
"Activity data is published in versions. get_activity_snapshot's snapshot_id binds only Activity capabilities, Activity rankings and program payouts. Catalog/feed tools use snapshot=start then meta.snapshot instead; do not send an Activity snapshot_id to find_programs. Never mix rows from different versions.",
|
|
485
|
+
"Preserve the user's thresholds, periods and platform scope when discovery times out or returns no matches. A sampled recent-change feed does not enumerate all supported platforms. Inspect partial_success, errors, ranking_scope and scan before claiming complete coverage.",
|
|
478
486
|
"Partial program name + action: run_program_name_action. Partial program name only: resolve_program.",
|
|
479
487
|
"Default to bounty-eligible in-scope data. Include out-of-scope, ineligible, VDP, disclosure-only, or no-bounty data only when the user explicitly asks.",
|
|
480
488
|
"Recently updated programs across platforms: find_recently_updated_programs. Do not infer a platform such as Immunefi unless the user explicitly names it.",
|
|
@@ -560,7 +568,7 @@ export function createBbradarServer(client, config) {
|
|
|
560
568
|
}));
|
|
561
569
|
registerTool("get_activity_snapshot", {
|
|
562
570
|
title: "Get Current Activity Version",
|
|
563
|
-
description: "Read only the identity of the currently published Activity version: snapshot_id, frozen as_of cutoff, expires_at, policy_version and suggested poll_seconds.
|
|
571
|
+
description: "Read only the identity of the currently published Activity version: snapshot_id, frozen as_of cutoff, expires_at, policy_version and suggested poll_seconds. Pass snapshot_id as snapshot only to get_activity_capabilities, get_activity_rankings and get_program_payouts. Catalog/feed tools such as find_programs use snapshot=start then meta.snapshot instead. as_of is when BBRadar froze its calculation, not a provider payment or update time.",
|
|
564
572
|
inputSchema: z.object({}).strict(),
|
|
565
573
|
outputSchema: { ...apiEnvelopeOutputShape(), snapshot_id: z.string().optional(), as_of: z.string().optional(), expires_at: z.string().optional(), policy_version: z.string().optional(), poll_seconds: z.number().optional() },
|
|
566
574
|
annotations: readOnlyAnnotations
|
|
@@ -918,6 +926,7 @@ export function createBbradarServer(client, config) {
|
|
|
918
926
|
program_id: programIdentifierSchema,
|
|
919
927
|
search: searchTextSchema.optional(),
|
|
920
928
|
target_type: filterValueSchema().optional(),
|
|
929
|
+
language_tags: languageTagListSchema().describe("Keep targets with any requested language tag. Applied while paging; repeat with the returned next_offset."),
|
|
921
930
|
min_target_score: z.number().int().min(0).max(100).optional(),
|
|
922
931
|
target_opportunity_label: targetOpportunityLabelSchema.optional(),
|
|
923
932
|
competition_risk: competitionRiskSchema.optional(),
|
|
@@ -1195,7 +1204,7 @@ export function createBbradarServer(client, config) {
|
|
|
1195
1204
|
include_ineligible: z.boolean().default(false).describe("Include ineligible."),
|
|
1196
1205
|
recent_changes_page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
1197
1206
|
max_results: z.number().int().min(1).max(MAX_RECENT_CHANGES).default(10),
|
|
1198
|
-
include_full_target_list: z.boolean().default(true).describe("Fetch the newest matching program
|
|
1207
|
+
include_full_target_list: z.boolean().default(true).describe("Fetch up to full_target_limit targets for the newest matching program. Check full_targets_has_more and continue with get_program_targets at full_targets_next_offset. Set false for the fastest cross-program discovery."),
|
|
1199
1208
|
full_target_list_mode: targetListModeSchema.default("identifiers"),
|
|
1200
1209
|
full_target_limit: z.number().int().min(1).max(MAX_TARGETS_PER_PROGRAM).default(MAX_TARGETS_PER_PROGRAM),
|
|
1201
1210
|
full_list_include_out_of_scope: z.boolean().default(false),
|
|
@@ -2502,6 +2511,7 @@ function bbradarGuide(topic, profile, client) {
|
|
|
2502
2511
|
decision_tree: decisionTree.filter((entry) => toolAvailableForContract(client, String(entry.first_tool)) &&
|
|
2503
2512
|
(topic === "start" || guideEntryMatchesTopic(topic, entry))),
|
|
2504
2513
|
filters,
|
|
2514
|
+
activity_catalog: activityCatalogGuidance,
|
|
2505
2515
|
advanced_tools: advancedTools,
|
|
2506
2516
|
examples
|
|
2507
2517
|
};
|
|
@@ -2580,7 +2590,8 @@ function coreBbradarGuide(topic, client) {
|
|
|
2580
2590
|
],
|
|
2581
2591
|
decision_tree: decisionTree.filter((entry) => toolAvailableForContract(client, String(entry.first_tool)) &&
|
|
2582
2592
|
(topic === "start" || guideEntryMatchesTopic(topic, entry))),
|
|
2583
|
-
available_tools: [...CORE_TOOL_NAMES].filter((name) => toolAvailableForContract(client, name))
|
|
2593
|
+
available_tools: [...CORE_TOOL_NAMES].filter((name) => toolAvailableForContract(client, name)),
|
|
2594
|
+
activity_catalog: activityCatalogGuidance
|
|
2584
2595
|
};
|
|
2585
2596
|
}
|
|
2586
2597
|
function toolAvailableForContract(client, toolName) {
|
|
@@ -3590,29 +3601,14 @@ async function runResolvedProgramAction(client, config, programId, input) {
|
|
|
3590
3601
|
});
|
|
3591
3602
|
}
|
|
3592
3603
|
if (input.action === "targets") {
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
const sanitizedTargets = rawTargets
|
|
3597
|
-
.map(sanitizeTarget)
|
|
3598
|
-
.filter((target) => targetHasAllowedScope(target, {
|
|
3604
|
+
return getProgramTargetsPayload(client, {
|
|
3605
|
+
target_type: input.target_type,
|
|
3606
|
+
language_tags: input.language_tags,
|
|
3599
3607
|
include_out_of_scope: input.include_out_of_scope,
|
|
3600
3608
|
include_ineligible: input.include_ineligible,
|
|
3601
|
-
strict_scope_filter: true
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
return withApiMetadata(api, {
|
|
3605
|
-
program_id: programId,
|
|
3606
|
-
targets: formatTargetList(targets, input.target_list_mode),
|
|
3607
|
-
meta: {
|
|
3608
|
-
limit: input.max_targets,
|
|
3609
|
-
output_mode: input.target_list_mode,
|
|
3610
|
-
total_active_targets: rawTargets.length,
|
|
3611
|
-
total_after_filters: sanitizedTargets.length,
|
|
3612
|
-
returned: targets.length,
|
|
3613
|
-
has_more: sanitizedTargets.length > targets.length
|
|
3614
|
-
}
|
|
3615
|
-
});
|
|
3609
|
+
strict_scope_filter: true,
|
|
3610
|
+
offset: 0, limit: input.max_targets, output_mode: input.target_list_mode
|
|
3611
|
+
}, programId);
|
|
3616
3612
|
}
|
|
3617
3613
|
return checkProgramNewTargets(client, config, {
|
|
3618
3614
|
program_id: programId,
|
|
@@ -4738,30 +4734,18 @@ async function getLatestAddedTargets(client, config, input) {
|
|
|
4738
4734
|
.map((change) => readObject(change.target))
|
|
4739
4735
|
.filter((target) => target !== undefined);
|
|
4740
4736
|
let fullTargets = [];
|
|
4741
|
-
let
|
|
4742
|
-
let totalAfterFilters;
|
|
4737
|
+
let fullTargetsMeta;
|
|
4743
4738
|
if (input.include_full_target_list && programId) {
|
|
4744
|
-
const
|
|
4745
|
-
const targetsData = readObject(targetsApi.data);
|
|
4746
|
-
const rawTargets = readArray(targetsData?.targets);
|
|
4747
|
-
fullTargets = rawTargets
|
|
4748
|
-
.map(sanitizeTarget)
|
|
4749
|
-
.filter((target) => targetHasAllowedScope(target, {
|
|
4739
|
+
const payload = await getProgramTargetsPayload(client, {
|
|
4750
4740
|
include_out_of_scope: input.full_list_include_out_of_scope,
|
|
4751
4741
|
include_ineligible: input.full_list_include_ineligible,
|
|
4752
|
-
strict_scope_filter: false
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
program_id: programId,
|
|
4759
|
-
request_id: targetsApi.requestId,
|
|
4760
|
-
upstream_request_id: targetsApi.upstreamRequestId,
|
|
4761
|
-
...apiSourceMetadata(targetsApi)
|
|
4762
|
-
});
|
|
4742
|
+
strict_scope_filter: false,
|
|
4743
|
+
offset: 0, limit: input.full_target_limit, output_mode: input.full_target_list_mode
|
|
4744
|
+
}, programId);
|
|
4745
|
+
fullTargets = readArray(payload.targets);
|
|
4746
|
+
fullTargetsMeta = readObject(payload.meta);
|
|
4747
|
+
sourceRequests.push(...readArray(payload.source_requests).map(readObject).filter((request) => request !== undefined));
|
|
4763
4748
|
}
|
|
4764
|
-
const limitedFullTargets = fullTargets.slice(0, input.full_target_limit);
|
|
4765
4749
|
return stripUndefined({
|
|
4766
4750
|
request_id: randomUUID(),
|
|
4767
4751
|
source_requests: sourceRequests,
|
|
@@ -4770,17 +4754,19 @@ async function getLatestAddedTargets(client, config, input) {
|
|
|
4770
4754
|
program,
|
|
4771
4755
|
matches,
|
|
4772
4756
|
new_targets: formatTargetList(latestNewTargets, input.full_target_list_mode),
|
|
4773
|
-
full_targets: input.include_full_target_list ?
|
|
4757
|
+
full_targets: input.include_full_target_list ? fullTargets : undefined,
|
|
4774
4758
|
meta: stripUndefined({
|
|
4775
4759
|
recent_changes_scanned: changes.length,
|
|
4776
4760
|
matching_changes_scanned: matchingChanges.length,
|
|
4777
4761
|
matches_returned: matches.length,
|
|
4778
4762
|
matches_has_more: matches.length < matchingChanges.length,
|
|
4779
4763
|
new_targets_returned: formatTargetList(latestNewTargets, input.full_target_list_mode).length,
|
|
4780
|
-
full_targets_total_active:
|
|
4781
|
-
full_targets_total_after_filters:
|
|
4782
|
-
full_targets_returned: input.include_full_target_list ?
|
|
4783
|
-
full_targets_has_more:
|
|
4764
|
+
full_targets_total_active: fullTargetsMeta?.total_active_targets,
|
|
4765
|
+
full_targets_total_after_filters: fullTargetsMeta?.total_after_filters,
|
|
4766
|
+
full_targets_returned: input.include_full_target_list ? fullTargets.length : undefined,
|
|
4767
|
+
full_targets_has_more: fullTargetsMeta?.has_more,
|
|
4768
|
+
full_targets_next_offset: fullTargetsMeta?.next_offset,
|
|
4769
|
+
full_targets_possibly_incomplete: fullTargetsMeta?.possibly_incomplete
|
|
4784
4770
|
})
|
|
4785
4771
|
});
|
|
4786
4772
|
}
|
|
@@ -4818,15 +4804,21 @@ function latestAddedProgramMatches(change, input, platformFilters) {
|
|
|
4818
4804
|
return true;
|
|
4819
4805
|
}
|
|
4820
4806
|
async function getProgramTargetsPayload(client, input, programId) {
|
|
4821
|
-
const
|
|
4807
|
+
const maxPageSize = client.endpointNumber("program_targets", "max_page_size", MAX_TARGETS_PER_PROGRAM);
|
|
4822
4808
|
// `eligibility=eligible` is an API-level in-scope bounty-eligibility filter,
|
|
4823
4809
|
// so the common eligible-only path needs no second local scope pass.
|
|
4824
|
-
const
|
|
4810
|
+
const languageTags = new Set((input.language_tags ?? []).map(normalizeTag));
|
|
4811
|
+
const directOffsetPaging = languageTags.size === 0 && (!input.include_ineligible || (!input.strict_scope_filter && input.include_out_of_scope));
|
|
4812
|
+
// Bound direct reads to the requested prefix. Local post-filters still need
|
|
4813
|
+
// full pages to find sparse matches. Offsets remain filtered row offsets,
|
|
4814
|
+
// independent of the page size used by an earlier call.
|
|
4815
|
+
const pageSize = directOffsetPaging ? Math.min(maxPageSize, input.offset + input.limit) : maxPageSize;
|
|
4825
4816
|
const collected = [];
|
|
4826
4817
|
let matchedSeen = directOffsetPaging ? Math.floor(input.offset / pageSize) * pageSize : 0;
|
|
4827
4818
|
let page = directOffsetPaging ? Math.floor(input.offset / pageSize) + 1 : 1;
|
|
4828
4819
|
const firstPage = page;
|
|
4829
4820
|
let api;
|
|
4821
|
+
const sourceRequests = [];
|
|
4830
4822
|
let upstreamMeta;
|
|
4831
4823
|
let upstreamHasMore = false;
|
|
4832
4824
|
do {
|
|
@@ -4840,11 +4832,15 @@ async function getProgramTargetsPayload(client, input, programId) {
|
|
|
4840
4832
|
page,
|
|
4841
4833
|
page_size: pageSize
|
|
4842
4834
|
});
|
|
4835
|
+
sourceRequests.push({ source: "program_targets", program_id: programId, page,
|
|
4836
|
+
request_id: api.requestId, upstream_request_id: api.upstreamRequestId, ...apiSourceMetadata(api) });
|
|
4843
4837
|
const data = readObject(api.data);
|
|
4844
4838
|
upstreamMeta = readObject(data?.meta);
|
|
4845
4839
|
for (const target of readArray(data?.targets).map(sanitizeTarget)) {
|
|
4846
4840
|
if (!targetHasAllowedScope(target, input))
|
|
4847
4841
|
continue;
|
|
4842
|
+
if (languageTags.size > 0 && !arraysIntersectNormalized(languageTags, readStringArrayField(target, "language_tags")))
|
|
4843
|
+
continue;
|
|
4848
4844
|
if (matchedSeen >= input.offset && collected.length < input.limit)
|
|
4849
4845
|
collected.push(target);
|
|
4850
4846
|
matchedSeen += 1;
|
|
@@ -4862,8 +4858,10 @@ async function getProgramTargetsPayload(client, input, programId) {
|
|
|
4862
4858
|
throw new Error("No program target response was collected.");
|
|
4863
4859
|
}
|
|
4864
4860
|
const targets = collected;
|
|
4861
|
+
const hasMore = upstreamHasMore || matchedSeen > input.offset + targets.length;
|
|
4865
4862
|
return withApiMetadata(api, {
|
|
4866
4863
|
program_id: programId,
|
|
4864
|
+
source_requests: sourceRequests,
|
|
4867
4865
|
targets: formatTargetList(targets, input.output_mode),
|
|
4868
4866
|
meta: {
|
|
4869
4867
|
offset: input.offset,
|
|
@@ -4872,7 +4870,8 @@ async function getProgramTargetsPayload(client, input, programId) {
|
|
|
4872
4870
|
total_active_targets: readNumber(upstreamMeta?.total) ?? matchedSeen,
|
|
4873
4871
|
total_after_filters: upstreamHasMore ? undefined : matchedSeen,
|
|
4874
4872
|
returned: targets.length,
|
|
4875
|
-
has_more:
|
|
4873
|
+
has_more: hasMore,
|
|
4874
|
+
next_offset: hasMore ? input.offset + targets.length : undefined,
|
|
4876
4875
|
upstream_has_more: upstreamHasMore,
|
|
4877
4876
|
possibly_incomplete: upstreamHasMore,
|
|
4878
4877
|
upstream_pages_fetched: page - firstPage,
|
|
@@ -4885,9 +4884,16 @@ async function getProgramTargetsPayload(client, input, programId) {
|
|
|
4885
4884
|
});
|
|
4886
4885
|
}
|
|
4887
4886
|
async function getProgramTargetsExportFallbackPayload(client, input, programId) {
|
|
4887
|
+
if (input.search || input.target_type || input.min_target_score !== undefined
|
|
4888
|
+
|| input.target_opportunity_label || input.competition_risk) {
|
|
4889
|
+
throw new Error("The per-program targets endpoint is unavailable. Its search, type and intelligence filters cannot be preserved by the export fallback; the requested filters were not discarded.");
|
|
4890
|
+
}
|
|
4888
4891
|
const exportLookup = await fetchProgramTargetsWithEndpointFallback(client, programId);
|
|
4889
|
-
const
|
|
4892
|
+
const languageTags = new Set((input.language_tags ?? []).map(normalizeTag));
|
|
4893
|
+
const sanitizedTargets = exportLookup.rawTargets.filter((target) => targetHasAllowedScope(target, input)
|
|
4894
|
+
&& (languageTags.size === 0 || arraysIntersectNormalized(languageTags, readStringArrayField(target, "language_tags"))));
|
|
4890
4895
|
const targets = sanitizedTargets.slice(input.offset, input.offset + input.limit);
|
|
4896
|
+
const hasMore = input.offset + targets.length < sanitizedTargets.length;
|
|
4891
4897
|
return stripUndefined({
|
|
4892
4898
|
request_id: exportLookup.requestId,
|
|
4893
4899
|
upstream_request_id: exportLookup.upstreamRequestId,
|
|
@@ -4901,10 +4907,10 @@ async function getProgramTargetsExportFallbackPayload(client, input, programId)
|
|
|
4901
4907
|
offset: input.offset,
|
|
4902
4908
|
limit: input.limit,
|
|
4903
4909
|
output_mode: input.output_mode,
|
|
4904
|
-
|
|
4905
|
-
total_after_filters: sanitizedTargets.length,
|
|
4910
|
+
...fallbackTargetCoverage(exportLookup, sanitizedTargets.length),
|
|
4906
4911
|
returned: targets.length,
|
|
4907
|
-
has_more:
|
|
4912
|
+
has_more: hasMore || exportLookup.coverage?.possibly_incomplete === true,
|
|
4913
|
+
next_offset: hasMore ? input.offset + targets.length : undefined,
|
|
4908
4914
|
target_source: exportLookup.source,
|
|
4909
4915
|
target_list_unavailable: exportLookup.unavailable,
|
|
4910
4916
|
fallback_error: exportLookup.error
|
|
@@ -4933,6 +4939,7 @@ async function fetchProgramTargetsWithEndpointFallback(client, programId) {
|
|
|
4933
4939
|
})),
|
|
4934
4940
|
source: "program_targets",
|
|
4935
4941
|
rawTargets,
|
|
4942
|
+
coverage: targetPageCoverage(data, rawTargets.length, rawTargets.length),
|
|
4936
4943
|
sourceRequests: [
|
|
4937
4944
|
{
|
|
4938
4945
|
source: "program_targets",
|
|
@@ -5032,14 +5039,21 @@ function cacheTtlMsFromDiagnostics(cache) {
|
|
|
5032
5039
|
}
|
|
5033
5040
|
async function fetchProgramTargetsFromExportFallback(client, programId) {
|
|
5034
5041
|
try {
|
|
5042
|
+
const limit = Math.min(MAX_TARGET_EXPORT, client.endpointNumber("target_export", "max_targets", MAX_TARGET_EXPORT));
|
|
5035
5043
|
const api = await client.exportTargets({
|
|
5036
5044
|
program_ids: [programId],
|
|
5037
5045
|
include_out_of_scope: true,
|
|
5038
5046
|
include_ineligible: true,
|
|
5039
5047
|
format: "json",
|
|
5040
|
-
limit
|
|
5048
|
+
limit
|
|
5041
5049
|
});
|
|
5042
5050
|
const rawTargets = readExportTargetRows(api.data).map((row) => sanitizeTarget(readObject(row)?.target ?? row));
|
|
5051
|
+
const data = readObject(api.data);
|
|
5052
|
+
const coverage = targetPageCoverage({ meta: {
|
|
5053
|
+
...readObject(data?.meta),
|
|
5054
|
+
// A full capped export is not proof that the inventory ends here.
|
|
5055
|
+
has_more: readObject(data?.meta)?.has_more === true || rawTargets.length >= limit
|
|
5056
|
+
} }, rawTargets.length, rawTargets.length);
|
|
5043
5057
|
return {
|
|
5044
5058
|
requestId: api.requestId,
|
|
5045
5059
|
upstreamRequestId: api.upstreamRequestId,
|
|
@@ -5053,6 +5067,7 @@ async function fetchProgramTargetsFromExportFallback(client, programId) {
|
|
|
5053
5067
|
})),
|
|
5054
5068
|
source: "target_export",
|
|
5055
5069
|
rawTargets,
|
|
5070
|
+
coverage,
|
|
5056
5071
|
sourceRequests: [
|
|
5057
5072
|
{
|
|
5058
5073
|
source: "target_export",
|
|
@@ -5313,8 +5328,7 @@ async function getProgramScopeSummary(client, config, input) {
|
|
|
5313
5328
|
program: formatProgram(lookup.program, "compact"),
|
|
5314
5329
|
scope: buildTargetScopeSummary(targets, input.target_list_mode, input.group_limit, input.include_out_of_scope, input.include_ineligible),
|
|
5315
5330
|
meta: {
|
|
5316
|
-
|
|
5317
|
-
total_after_filters: targets.length,
|
|
5331
|
+
...targetPageCoverage(targetsData, rawTargets.length, targets.length),
|
|
5318
5332
|
target_list_mode: input.target_list_mode,
|
|
5319
5333
|
group_limit: input.group_limit,
|
|
5320
5334
|
target_source: "program_targets",
|
|
@@ -5339,8 +5353,7 @@ async function getProgramScopeSummaryFromIndexFallback(client, indexFallback, in
|
|
|
5339
5353
|
program: formatProgram(indexFallback.program, "compact"),
|
|
5340
5354
|
scope: buildTargetScopeSummary(targets, input.target_list_mode, input.group_limit, input.include_out_of_scope, input.include_ineligible),
|
|
5341
5355
|
meta: stripUndefined({
|
|
5342
|
-
|
|
5343
|
-
total_after_filters: targets.length,
|
|
5356
|
+
...fallbackTargetCoverage(exportLookup, targets.length),
|
|
5344
5357
|
target_list_mode: input.target_list_mode,
|
|
5345
5358
|
group_limit: input.group_limit,
|
|
5346
5359
|
target_source: exportLookup.source,
|
|
@@ -5376,8 +5389,7 @@ async function getProgramTargetBreakdown(client, config, input) {
|
|
|
5376
5389
|
groups: readObject(scope.groups)
|
|
5377
5390
|
},
|
|
5378
5391
|
meta: {
|
|
5379
|
-
|
|
5380
|
-
total_after_filters: targets.length,
|
|
5392
|
+
...targetPageCoverage(targetsData, rawTargets.length, targets.length),
|
|
5381
5393
|
target_list_mode: input.target_list_mode,
|
|
5382
5394
|
group_limit: input.group_limit,
|
|
5383
5395
|
target_source: "program_targets",
|
|
@@ -5385,6 +5397,32 @@ async function getProgramTargetBreakdown(client, config, input) {
|
|
|
5385
5397
|
}
|
|
5386
5398
|
});
|
|
5387
5399
|
}
|
|
5400
|
+
function targetPageCoverage(data, rows, filteredRows) {
|
|
5401
|
+
const meta = readObject(data?.meta);
|
|
5402
|
+
const total = numberField(meta, "total");
|
|
5403
|
+
const incomplete = meta?.has_more === true || (numberField(meta, "total_pages") ?? 1) > 1
|
|
5404
|
+
|| (total !== undefined && total > rows);
|
|
5405
|
+
return stripUndefined({
|
|
5406
|
+
total_active_targets: total ?? (incomplete ? undefined : rows),
|
|
5407
|
+
total_after_filters: incomplete ? undefined : filteredRows,
|
|
5408
|
+
targets_scanned: rows,
|
|
5409
|
+
matching_targets_scanned: filteredRows,
|
|
5410
|
+
possibly_incomplete: incomplete,
|
|
5411
|
+
count_scope: incomplete ? "sampled_target_page" : "complete_target_inventory"
|
|
5412
|
+
});
|
|
5413
|
+
}
|
|
5414
|
+
function fallbackTargetCoverage(lookup, filteredRows) {
|
|
5415
|
+
const incomplete = lookup.unavailable || lookup.coverage?.possibly_incomplete === true;
|
|
5416
|
+
return stripUndefined({
|
|
5417
|
+
...lookup.coverage,
|
|
5418
|
+
total_active_targets: lookup.unavailable ? undefined : lookup.coverage?.total_active_targets ?? (incomplete ? undefined : lookup.rawTargets.length),
|
|
5419
|
+
total_after_filters: incomplete ? undefined : filteredRows,
|
|
5420
|
+
targets_scanned: lookup.rawTargets.length,
|
|
5421
|
+
matching_targets_scanned: filteredRows,
|
|
5422
|
+
possibly_incomplete: incomplete,
|
|
5423
|
+
count_scope: lookup.unavailable ? "unavailable" : incomplete ? "sampled_target_page" : "complete_target_inventory"
|
|
5424
|
+
});
|
|
5425
|
+
}
|
|
5388
5426
|
async function getProgramTargetBreakdownFromIndexFallback(client, indexFallback, input) {
|
|
5389
5427
|
const exportLookup = await fetchProgramTargetsWithEndpointFallback(client, input.program_id);
|
|
5390
5428
|
const targets = exportLookup.rawTargets.filter((target) => targetHasAllowedScope(target, {
|
|
@@ -5411,8 +5449,7 @@ async function getProgramTargetBreakdownFromIndexFallback(client, indexFallback,
|
|
|
5411
5449
|
groups: readObject(scope.groups)
|
|
5412
5450
|
},
|
|
5413
5451
|
meta: stripUndefined({
|
|
5414
|
-
|
|
5415
|
-
total_after_filters: targets.length,
|
|
5452
|
+
...fallbackTargetCoverage(exportLookup, targets.length),
|
|
5416
5453
|
target_list_mode: input.target_list_mode,
|
|
5417
5454
|
group_limit: input.group_limit,
|
|
5418
5455
|
target_source: exportLookup.source,
|
|
@@ -6820,6 +6857,18 @@ function clampLimit(name, value, max, warnings) {
|
|
|
6820
6857
|
// server order, so a larger pool lets ranking_mode choose among more of them.
|
|
6821
6858
|
const HUNT_SERVER_POOL = 100;
|
|
6822
6859
|
async function findHuntTargets(client, config, input, activity = {}) {
|
|
6860
|
+
const huntStartedAt = Date.now();
|
|
6861
|
+
const phaseTiming = {};
|
|
6862
|
+
const runStage = async (phase, timeoutMs, callback) => {
|
|
6863
|
+
const startedAt = Date.now();
|
|
6864
|
+
try {
|
|
6865
|
+
return await client.withRequestTimeout(timeoutMs, callback);
|
|
6866
|
+
}
|
|
6867
|
+
finally {
|
|
6868
|
+
phaseTiming[phase] = Date.now() - startedAt;
|
|
6869
|
+
}
|
|
6870
|
+
};
|
|
6871
|
+
validateActivityCatalogInput(activity);
|
|
6823
6872
|
const warnings = [];
|
|
6824
6873
|
const errors = [];
|
|
6825
6874
|
const selfHostedOnly = isSelfHostedOnlyPlatformSelection(input.platforms);
|
|
@@ -6885,22 +6934,28 @@ async function findHuntTargets(client, config, input, activity = {}) {
|
|
|
6885
6934
|
warnings.push(`Recent target-change queries were reduced from ${desiredRecentChangeQueries.length} to ${recentChangeQueries.length} to preserve the upstream request budget.`);
|
|
6886
6935
|
}
|
|
6887
6936
|
const serverFiltered = hasActivityCatalogInput(activity);
|
|
6937
|
+
// Catalog discovery and optional changes share at most 60% of the tool
|
|
6938
|
+
// deadline. Keep at least 30% for exact targets and 10% for the response.
|
|
6888
6939
|
const [programPayload, recentResult] = await Promise.all([
|
|
6889
|
-
serverFiltered
|
|
6940
|
+
runStage("program_discovery_ms", Math.floor(config.requestTimeoutMs * 0.6), () => serverFiltered
|
|
6890
6941
|
? findFilteredServerPrograms(client, config, {
|
|
6891
6942
|
...programInput, ...activity, opportunity_levels: [], sort_by: activity.sort_by, sort_direction: activity.sort_direction,
|
|
6892
6943
|
platforms: input.platforms, tags: input.tags, updated_since: input.updated_since, vdp_mode: "exclude", include_target_samples: false
|
|
6893
6944
|
}, {
|
|
6894
6945
|
...programInput, platforms: [], tags: [], vdp_mode: "include", updated_since: undefined,
|
|
6895
6946
|
max_pages: programRequestBudget, upstream_request_budget: programRequestBudget
|
|
6896
|
-
}, HUNT_SERVER_POOL)
|
|
6897
|
-
: findPrograms(client, config, programInput
|
|
6947
|
+
}, HUNT_SERVER_POOL, true)
|
|
6948
|
+
: findPrograms(client, config, programInput, {
|
|
6949
|
+
allowPartialTimeout: true, excludeVdpUpstream: true
|
|
6950
|
+
})),
|
|
6898
6951
|
recentChangesEnabled
|
|
6899
|
-
? fetchHuntRecentChanges(client, config, input, recentChangeQueries)
|
|
6952
|
+
? runStage("recent_changes_ms", Math.floor(config.requestTimeoutMs * 0.3), () => fetchHuntRecentChanges(client, config, input, recentChangeQueries))
|
|
6900
6953
|
: Promise.resolve({ changes: [], sourceRequests: [], requestsUsed: 0, errors: [] })
|
|
6901
6954
|
]);
|
|
6955
|
+
client.throwIfRequestAborted();
|
|
6902
6956
|
const programWarnings = readArray(programPayload.warnings).filter((warning) => typeof warning === "string");
|
|
6903
6957
|
warnings.push(...programWarnings);
|
|
6958
|
+
errors.push(...readArray(programPayload.errors).map(readObject).filter((error) => error !== undefined));
|
|
6904
6959
|
errors.push(...recentResult.errors);
|
|
6905
6960
|
const programBudget = readObject(programPayload.upstream_budget);
|
|
6906
6961
|
const programRequestsUsed = readNumber(programBudget?.used) ?? 0;
|
|
@@ -6919,8 +6974,12 @@ async function findHuntTargets(client, config, input, activity = {}) {
|
|
|
6919
6974
|
const finalistEntries = preliminaryRankedEntries
|
|
6920
6975
|
.slice(0, sampledProgramCount)
|
|
6921
6976
|
.map((entry) => entry.entry);
|
|
6922
|
-
const targetResult = await collectHuntTargetSamples(client, finalistEntries, input);
|
|
6977
|
+
const targetResult = await runStage("target_sampling_ms", Math.floor(config.requestTimeoutMs * 0.9) - (Date.now() - huntStartedAt), () => collectHuntTargetSamples(client, finalistEntries, input));
|
|
6978
|
+
client.throwIfRequestAborted();
|
|
6923
6979
|
errors.push(...targetResult.errors);
|
|
6980
|
+
if (errors.length > 0) {
|
|
6981
|
+
warnings.push("Some discovery or enrichment reads failed or reached their time budget. Returned matches preserve the requested filters; missing target samples or change evidence are unavailable, not confirmed absent. Report the incomplete coverage without narrowing platforms or relaxing thresholds.");
|
|
6982
|
+
}
|
|
6924
6983
|
if (sampledProgramCount < Math.min(input.max_results, programEntries.length)) {
|
|
6925
6984
|
warnings.push(`The upstream request budget limited the final exact target shortlist to ${sampledProgramCount} of ${Math.min(input.max_results, programEntries.length)} requested results.`);
|
|
6926
6985
|
}
|
|
@@ -6934,6 +6993,10 @@ async function findHuntTargets(client, config, input, activity = {}) {
|
|
|
6934
6993
|
}));
|
|
6935
6994
|
const targetRequestsUsed = targetResult.requestsUsed;
|
|
6936
6995
|
const totalRequestsUsed = programRequestsUsed + recentResult.requestsUsed + targetRequestsUsed;
|
|
6996
|
+
const coverageIncomplete = errors.length > 0
|
|
6997
|
+
|| readObject(programPayload.ranking_scope)?.possibly_incomplete === true
|
|
6998
|
+
|| readObject(programPayload.scan)?.complete === false
|
|
6999
|
+
|| sampledProgramCount < Math.min(input.max_results, programEntries.length);
|
|
6937
7000
|
return stripUndefined({
|
|
6938
7001
|
request_id: randomUUID(),
|
|
6939
7002
|
fetched_at: new Date().toISOString(),
|
|
@@ -6985,7 +7048,10 @@ async function findHuntTargets(client, config, input, activity = {}) {
|
|
|
6985
7048
|
result_guidance: {
|
|
6986
7049
|
final_shortlist: true,
|
|
6987
7050
|
follow_up_calls_required: false,
|
|
6988
|
-
|
|
7051
|
+
coverage_incomplete: coverageIncomplete,
|
|
7052
|
+
instruction: coverageIncomplete
|
|
7053
|
+
? "Use the bounded matches and report warnings and incomplete coverage. Preserve the user's periods, thresholds and platform scope; do not infer supported platforms from a sampled recent-change feed."
|
|
7054
|
+
: "Use these matches as the final shortlist. Exact bounded target samples and recent additions are already included; call get_program_targets only if the user explicitly requests an exhaustive scope dump for one selected program."
|
|
6989
7055
|
},
|
|
6990
7056
|
matches,
|
|
6991
7057
|
meta: {
|
|
@@ -6998,7 +7064,8 @@ async function findHuntTargets(client, config, input, activity = {}) {
|
|
|
6998
7064
|
model_round_trips_expected: 1,
|
|
6999
7065
|
parallel_recent_change_fetch: recentChangeQueries.length > 0,
|
|
7000
7066
|
bulk_resource_links_suppressed: true,
|
|
7001
|
-
partial_success: errors.length > 0
|
|
7067
|
+
partial_success: errors.length > 0,
|
|
7068
|
+
phase_timing: phaseTiming
|
|
7002
7069
|
}
|
|
7003
7070
|
});
|
|
7004
7071
|
}
|
|
@@ -7738,6 +7805,7 @@ function hasActivityCatalogInput(input) {
|
|
|
7738
7805
|
}
|
|
7739
7806
|
/** Validates and builds one server-ordered catalog query; nothing is sent. */
|
|
7740
7807
|
function serverCatalogQuery(client, input) {
|
|
7808
|
+
validateActivityCatalogInput(input);
|
|
7741
7809
|
const levels = Array.isArray(input.opportunity_levels) ? input.opportunity_levels : [];
|
|
7742
7810
|
if (levels.length > 1)
|
|
7743
7811
|
throw new Error("Combined Activity queries accept at most one opportunity tier; separate tier pages cannot form a global ranking.");
|
|
@@ -7817,6 +7885,7 @@ async function scanServerCatalog(client, config, input, local, limits) {
|
|
|
7817
7885
|
const start = cursorValue ? decodeScanCursor(cursorValue, fingerprint, pageSize)
|
|
7818
7886
|
: { v: 1, p: Number(input.page ?? 1), k: 0, s: pageSize, f: fingerprint, a: undefined, t: undefined };
|
|
7819
7887
|
const sourceRequests = [];
|
|
7888
|
+
const errors = [];
|
|
7820
7889
|
let matches = [];
|
|
7821
7890
|
let seen = new Set();
|
|
7822
7891
|
let nextPage = start.p;
|
|
@@ -7840,11 +7909,25 @@ async function scanServerCatalog(client, config, input, local, limits) {
|
|
|
7840
7909
|
const needed = pagesRead === 0 ? 1 : Math.ceil((limits.maxResults - matches.length) / (rate * pageSize));
|
|
7841
7910
|
const batch = Math.max(1, Math.min(allowance, needed, CATALOG_SCAN_PREFETCH));
|
|
7842
7911
|
const pageToken = token;
|
|
7843
|
-
const
|
|
7912
|
+
const batchStart = nextPage;
|
|
7913
|
+
const pages = await Promise.allSettled(Array.from({ length: batch }, (_, index) => batchStart + index)
|
|
7844
7914
|
.map(async (page) => ({ page, api: await readServerCatalogPage(client, catalog, page, pageSize, pageToken) })));
|
|
7845
|
-
|
|
7915
|
+
// Count all admitted reads, including a failed page or a prefetched page
|
|
7916
|
+
// beyond a gap; only the contiguous successful prefix enters the ranking.
|
|
7917
|
+
for (const result of pages) {
|
|
7918
|
+
const noRequest = result.status === "fulfilled" ? result.value.api.cached || result.value.api.coalesced
|
|
7919
|
+
: result.reason instanceof BBRadarApiError && (result.reason.cached || result.reason.coalesced);
|
|
7920
|
+
limits.budget.remaining -= noRequest ? 0 : 1;
|
|
7921
|
+
}
|
|
7922
|
+
for (const [index, result] of pages.entries()) {
|
|
7923
|
+
if (result.status === "rejected") {
|
|
7924
|
+
if (!limits.allowPartialTimeout || pagesRead === 0 || !(result.reason instanceof BBRadarApiError) || result.reason.status !== 408)
|
|
7925
|
+
throw result.reason;
|
|
7926
|
+
errors.push(apiOperationError(`program catalog page ${batchStart + index}`, result.reason));
|
|
7927
|
+
break scan;
|
|
7928
|
+
}
|
|
7929
|
+
const { page, api } = result.value;
|
|
7846
7930
|
pagesRead += 1;
|
|
7847
|
-
limits.budget.remaining -= api.cached || api.coalesced ? 0 : 1;
|
|
7848
7931
|
sourceRequests.push({ source: catalog.level ? `opportunities:${catalog.level}` : "programs", page, request_id: api.requestId,
|
|
7849
7932
|
upstream_request_id: api.upstreamRequestId, ...apiSourceMetadata(api) });
|
|
7850
7933
|
const data = readObject(api.data);
|
|
@@ -7900,6 +7983,7 @@ async function scanServerCatalog(client, config, input, local, limits) {
|
|
|
7900
7983
|
return {
|
|
7901
7984
|
candidates: matches,
|
|
7902
7985
|
sourceRequests,
|
|
7986
|
+
errors,
|
|
7903
7987
|
scan: stripUndefined({
|
|
7904
7988
|
order: "Server order; local filters only remove programs.",
|
|
7905
7989
|
sort_by: input.sort_by ?? "updated",
|
|
@@ -7913,17 +7997,20 @@ async function scanServerCatalog(client, config, input, local, limits) {
|
|
|
7913
7997
|
? encodeScanCursor({ v: 1, p: resume.p, k: resume.k, s: pageSize, f: fingerprint, a: version, t: typeof token === "string" ? token : undefined })
|
|
7914
7998
|
: undefined,
|
|
7915
7999
|
activity_snapshot_id: version,
|
|
7916
|
-
restarted_on_new_activity_version: restarted || undefined
|
|
8000
|
+
restarted_on_new_activity_version: restarted || undefined,
|
|
8001
|
+
deadline_reached: errors.length > 0 || undefined
|
|
7917
8002
|
})
|
|
7918
8003
|
};
|
|
7919
8004
|
}
|
|
7920
8005
|
/** find_programs with server Activity filters plus local filters or target samples. */
|
|
7921
|
-
async function findFilteredServerPrograms(client, config, input, local, maxResults = local.max_results) {
|
|
8006
|
+
async function findFilteredServerPrograms(client, config, input, local, maxResults = local.max_results, allowPartialTimeout = false) {
|
|
7922
8007
|
const normalized = normalizeFindProgramsInput(local);
|
|
7923
8008
|
const warnings = normalized.warnings;
|
|
7924
8009
|
local = normalized.input;
|
|
7925
8010
|
const budget = { initial: local.upstream_request_budget, remaining: local.upstream_request_budget };
|
|
7926
|
-
const result = await scanServerCatalog(client, config, input, local, { maxResults, maxPages: local.max_pages, budget });
|
|
8011
|
+
const result = await scanServerCatalog(client, config, input, local, { maxResults, maxPages: local.max_pages, budget, allowPartialTimeout });
|
|
8012
|
+
if (result.errors.length > 0)
|
|
8013
|
+
warnings.push("Program discovery reached its time budget; ranking covers only the completed server-ordered prefix.");
|
|
7927
8014
|
if (result.scan.complete !== true) {
|
|
7928
8015
|
warnings.push("More server pages remain; scan.next_cursor continues in the same order.");
|
|
7929
8016
|
}
|
|
@@ -7936,6 +8023,7 @@ async function findFilteredServerPrograms(client, config, input, local, maxResul
|
|
|
7936
8023
|
request_id: randomUUID(),
|
|
7937
8024
|
fetched_at: new Date().toISOString(),
|
|
7938
8025
|
source_requests: result.sourceRequests,
|
|
8026
|
+
errors: result.errors.length > 0 ? result.errors : undefined,
|
|
7939
8027
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
7940
8028
|
upstream_budget: { used: budget.initial - budget.remaining, remaining: budget.remaining },
|
|
7941
8029
|
ranking_scope: { mode: "server_filtered", sort_by: result.scan.sort_by, sort_direction: result.scan.sort_direction, complete: result.scan.complete },
|
|
@@ -7950,10 +8038,11 @@ function programHasNoSubmissionFee(program) {
|
|
|
7950
8038
|
const amount = numberField(cost, "amount");
|
|
7951
8039
|
return amount === undefined || (amount === 0 && (numberField(cost, "maximum_amount") ?? 0) === 0);
|
|
7952
8040
|
}
|
|
7953
|
-
async function findPrograms(client, config, input) {
|
|
8041
|
+
async function findPrograms(client, config, input, execution = {}) {
|
|
7954
8042
|
const normalized = normalizeFindProgramsInput(input);
|
|
7955
8043
|
const warnings = normalized.warnings;
|
|
7956
8044
|
input = normalized.input;
|
|
8045
|
+
const errors = [];
|
|
7957
8046
|
const budget = {
|
|
7958
8047
|
initial: input.upstream_request_budget,
|
|
7959
8048
|
remaining: input.upstream_request_budget
|
|
@@ -7970,11 +8059,13 @@ async function findPrograms(client, config, input) {
|
|
|
7970
8059
|
platforms: input.platforms,
|
|
7971
8060
|
tags: upstreamTags,
|
|
7972
8061
|
program_kind: input.program_kind ?? (input.contest_like ? "contest" : undefined),
|
|
8062
|
+
vdp_mode: execution.excludeVdpUpstream && input.vdp_mode === "exclude" ? "exclude" : undefined,
|
|
7973
8063
|
updated_since: input.updated_since,
|
|
7974
8064
|
opportunity_levels: input.opportunity_levels,
|
|
7975
8065
|
max_pages: input.max_pages,
|
|
7976
8066
|
budget,
|
|
7977
|
-
warnings
|
|
8067
|
+
warnings,
|
|
8068
|
+
partialTimeoutErrors: execution.allowPartialTimeout ? errors : undefined
|
|
7978
8069
|
});
|
|
7979
8070
|
const filteredCandidates = collected.programs
|
|
7980
8071
|
.map((program) => toProgramCandidate(program, config.webBaseUrl))
|
|
@@ -7998,6 +8089,7 @@ async function findPrograms(client, config, input) {
|
|
|
7998
8089
|
source_requests: collected.sourceRequests,
|
|
7999
8090
|
fetched_at: new Date().toISOString(),
|
|
8000
8091
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
8092
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
8001
8093
|
filters: stripUndefined({
|
|
8002
8094
|
platforms: input.platforms.length > 0 ? input.platforms : undefined,
|
|
8003
8095
|
exclude_platforms: input.exclude_platforms && input.exclude_platforms.length > 0 ? input.exclude_platforms : undefined,
|
|
@@ -8040,7 +8132,7 @@ async function findPrograms(client, config, input) {
|
|
|
8040
8132
|
platform_filter: input.platforms.length > 0 ? input.platforms : "all_platforms",
|
|
8041
8133
|
max_pages_scanned_per_source: input.max_pages,
|
|
8042
8134
|
upstream_total_pages: collected.totalPagesBySource,
|
|
8043
|
-
possibly_incomplete: isRankingPossiblyIncomplete(collected.totalPagesBySource, input.max_pages)
|
|
8135
|
+
possibly_incomplete: errors.length > 0 || isRankingPossiblyIncomplete(collected.totalPagesBySource, input.max_pages)
|
|
8044
8136
|
},
|
|
8045
8137
|
programs,
|
|
8046
8138
|
meta: {
|
|
@@ -8445,20 +8537,39 @@ async function collectProgramSource(client, source, sourceIndex, options) {
|
|
|
8445
8537
|
async function fetchProgramPage(client, source, sourceIndex, page, options) {
|
|
8446
8538
|
const runWithSlot = options.runWithProgramPageSlot ?? runImmediately;
|
|
8447
8539
|
return runWithSlot(async () => {
|
|
8540
|
+
const endpoint = source.kind === "programs" ? "programs" : "opportunities";
|
|
8541
|
+
const vdpModes = readArray(readObject(client.endpointCapability(endpoint)?.catalog_filters)?.vdp_mode);
|
|
8448
8542
|
const query = toQuery({
|
|
8449
8543
|
search: options.search,
|
|
8450
8544
|
platforms: options.platforms,
|
|
8451
8545
|
tags: options.tags,
|
|
8452
8546
|
updated_since: options.updated_since,
|
|
8453
8547
|
program_kind: source.kind === "programs" ? options.program_kind : undefined,
|
|
8548
|
+
// This removes only known VDPs; unknown classifications with positive
|
|
8549
|
+
// rewards still reach the local filters. Older APIs keep local exclusion.
|
|
8550
|
+
vdp_mode: supportsCatalogFilters(client, endpoint) && vdpModes.includes("exclude") ? options.vdp_mode : undefined,
|
|
8454
8551
|
page,
|
|
8455
|
-
page_size: MAX_PROGRAMS_PER_SEARCH
|
|
8552
|
+
page_size: Math.min(MAX_PROGRAMS_PER_SEARCH, client.endpointNumber(endpoint, "max_page_size", MAX_PROGRAMS_PER_SEARCH))
|
|
8456
8553
|
});
|
|
8457
8554
|
if (!tryConsumeBudget(options.budget)) {
|
|
8458
8555
|
addUniqueWarning(options.warnings, "Upstream request budget was exhausted before all requested program pages could be fetched.");
|
|
8459
8556
|
return undefined;
|
|
8460
8557
|
}
|
|
8461
|
-
|
|
8558
|
+
let api;
|
|
8559
|
+
try {
|
|
8560
|
+
api = source.kind === "opportunity" ? await client.getOpportunities(source.level, query) : await client.listPrograms(query);
|
|
8561
|
+
}
|
|
8562
|
+
catch (error) {
|
|
8563
|
+
// Hunts can retain completed catalog pages. Ordinary discovery keeps its
|
|
8564
|
+
// existing failure contract, and a failed first page is never an empty hit.
|
|
8565
|
+
if (page === 1 || !options.partialTimeoutErrors || !(error instanceof BBRadarApiError) || error.status !== 408)
|
|
8566
|
+
throw error;
|
|
8567
|
+
if (error.cached || error.coalesced)
|
|
8568
|
+
options.budget.remaining += 1;
|
|
8569
|
+
options.partialTimeoutErrors.push(apiOperationError(`program catalog page ${page}`, error));
|
|
8570
|
+
addUniqueWarning(options.warnings, "Program discovery reached its time budget; ranking covers only completed catalog pages.");
|
|
8571
|
+
return undefined;
|
|
8572
|
+
}
|
|
8462
8573
|
refundBudgetIfNoUpstreamRequest(options.budget, api);
|
|
8463
8574
|
const data = readObject(api.data);
|
|
8464
8575
|
const meta = readObject(data?.meta);
|
|
@@ -9280,6 +9391,7 @@ function compactToolText(payload, isError) {
|
|
|
9280
9391
|
}));
|
|
9281
9392
|
}
|
|
9282
9393
|
const meta = readObject(payload.meta);
|
|
9394
|
+
const coverageIncomplete = readObject(payload.result_guidance)?.coverage_incomplete === true;
|
|
9283
9395
|
const returned = numberField(meta, "returned") ??
|
|
9284
9396
|
[
|
|
9285
9397
|
readArray(payload.programs).length,
|
|
@@ -9291,6 +9403,12 @@ function compactToolText(payload, isError) {
|
|
|
9291
9403
|
request_id: payload.request_id,
|
|
9292
9404
|
summary: "Full tool result is available in structuredContent.",
|
|
9293
9405
|
returned,
|
|
9406
|
+
partial_success: meta?.partial_success === true ? true : undefined,
|
|
9407
|
+
coverage_incomplete: coverageIncomplete ? true : undefined,
|
|
9408
|
+
errors: meta?.partial_success === true ? readArray(payload.errors).map((error) => {
|
|
9409
|
+
const value = readObject(error);
|
|
9410
|
+
return stripUndefined({ operation: value?.operation, status: value?.status });
|
|
9411
|
+
}) : undefined,
|
|
9294
9412
|
mcp_timing: payload.mcp_timing
|
|
9295
9413
|
}));
|
|
9296
9414
|
}
|