@indexnetwork/protocol 8.1.0-rc.421.1 → 8.2.0-rc.423.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.
@@ -29,7 +29,9 @@ import { protocolLogger, withCallLogging } from '../../shared/observability/prot
29
29
  import { timed } from '../../shared/observability/performance.js';
30
30
  import { renderNetworkContext } from '../../shared/network/metadata.renderer.js';
31
31
  import { requestContext } from "../../shared/observability/request-context.js";
32
+ import { getAbortSignalConfig } from "../../shared/agent/model-signal.js";
32
33
  import { mergeOpportunityEvidence, withCandidateEvidence, withMatchedStrategies } from '../domain/opportunity.evidence.js';
34
+ import { discoveryIntentMatchingEnabled, discoveryProfileMatchingEnabled, discoveryProfileSource } from '../discovery.env.js';
33
35
  import { normalizeOpportunityActorIntent, resolveOpportunityActorIntent } from '../domain/opportunity.actor.js';
34
36
  import { approveOpportunityIntroduction, deleteOpportunityLifecycle, sendOpportunityLifecycle, updateOpportunityLifecycle } from './opportunity.lifecycle.js';
35
37
  import { stampEligibleNewbornOpportunities } from './opportunity.newborn-stamping.js';
@@ -55,6 +57,14 @@ const deleteLog = protocolLogger('OpportunityGraph:Delete');
55
57
  const sendLog = protocolLogger('OpportunityGraph:Send');
56
58
  const negotiateExistingLog = protocolLogger('OpportunityGraph:NegotiateExisting');
57
59
  const routingLog = protocolLogger('OpportunityGraph:Routing');
60
+ /**
61
+ * Error text can include provider response bodies, URLs, and credentials. Keep
62
+ * observability useful by retaining only a conservative error class at this
63
+ * boundary; detailed errors are intentionally not emitted from graph traces.
64
+ */
65
+ export function safeOpportunityGraphError(_error) {
66
+ return 'OpportunityEvaluationError: [redacted]';
67
+ }
58
68
  /** Time window for persist-node dedup. Suppresses a second opportunity with the same person while a recent one (within 30 days) is still in flight, so a person is not re-surfaced multiple times within a month (EDG-23). */
59
69
  const DEDUP_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
60
70
  const ACTIVE_NEGOTIATION_TASK_STATES = new Set([
@@ -120,7 +130,7 @@ function buildEvaluatorEvidenceKey(candidate) {
120
130
  return [
121
131
  candidate.candidateUserId,
122
132
  candidate.networkId,
123
- candidate.candidateIntentId ?? candidate.candidatePremiseId ?? candidate.sourceContextId ?? 'profile',
133
+ candidate.candidateIntentId ?? candidate.candidatePremiseId ?? candidate.candidateContextId ?? candidate.sourceContextId ?? 'profile',
124
134
  ].join(':');
125
135
  }
126
136
  /**
@@ -237,7 +247,7 @@ export class OpportunityGraphFactory {
237
247
  }
238
248
  catch (err) {
239
249
  const durationMs = Date.now() - nodeStart;
240
- const errMsg = err instanceof Error ? err.message : String(err);
250
+ const errMsg = safeOpportunityGraphError(err);
241
251
  traceEmitter?.({ type: "agent_end", name: traceName, durationMs, summary: `error: ${errMsg}` });
242
252
  throw err;
243
253
  }
@@ -287,8 +297,14 @@ export class OpportunityGraphFactory {
287
297
  // DISCOVERY_SOURCE_PREMISE_LIMIT. Loading all premises here caused
288
298
  // BACKEND-5: thousands of parallel vector searches for premise-rich users.
289
299
  const sourcePremises = [];
290
- const contextToIntentEnabled = process.env.DISCOVERY_CONTEXT_TO_INTENT !== '0';
291
- const rawContexts = contextToIntentEnabled && typeof this.database.getUserContexts === 'function'
300
+ const profileEnabled = discoveryProfileMatchingEnabled();
301
+ // Context-backed discovery is exclusively the user_context profile-source
302
+ // strategy. Premise mode must not load contexts or emit context-to-intent
303
+ // evidence merely because intent matching is also enabled.
304
+ const userContextProfileEnabled = profileEnabled && discoveryProfileSource() === 'user_context';
305
+ const contextToIntentEnabled = userContextProfileEnabled && process.env.DISCOVERY_CONTEXT_TO_INTENT !== '0';
306
+ const contextToContextEnabled = userContextProfileEnabled;
307
+ const rawContexts = (contextToIntentEnabled || contextToContextEnabled) && typeof this.database.getUserContexts === 'function'
292
308
  ? await this.database.getUserContexts(discoveryUserId)
293
309
  : [];
294
310
  const sourceContexts = rawContexts
@@ -731,6 +747,17 @@ export class OpportunityGraphFactory {
731
747
  const perIndexLimit = 80;
732
748
  // Similarity threshold for recall (0.30 = 30% similarity)
733
749
  const minScore = 0.3;
750
+ // Discovery match-type gating (DISCOVERY_ALLOWED_TYPES / DISCOVERY_PROFILE_SOURCE).
751
+ // Result filtering below is defense-in-depth: corpusGating already scopes the
752
+ // embedder's corpus searches, but adapters may still return other types.
753
+ const intentResultsEnabled = discoveryIntentMatchingEnabled();
754
+ const premiseResultsEnabled = discoveryProfileMatchingEnabled() && discoveryProfileSource() === 'premise';
755
+ const contextResultsEnabled = discoveryProfileMatchingEnabled() && discoveryProfileSource() === 'user_context';
756
+ const corpusGating = {
757
+ intents: discoveryIntentMatchingEnabled(),
758
+ profile: discoveryProfileMatchingEnabled(),
759
+ profileCorpus: discoveryProfileSource(),
760
+ };
734
761
  if (state.discoverySource === 'context') {
735
762
  // Context discovery: HyDE (when search query exists) + premise-to-premise.
736
763
  if (state.searchQuery?.trim()) {
@@ -791,26 +818,31 @@ export class OpportunityGraphFactory {
791
818
  model: getModelName("hydeGenerator"),
792
819
  },
793
820
  });
794
- const [premiseCands, contextCands] = await Promise.all([
821
+ const [premiseCands, contextCands, contextSimCands] = await Promise.all([
795
822
  runPremiseDiscovery(),
796
823
  runContextToIntentDiscovery(),
824
+ runContextToContextDiscovery(),
797
825
  ]);
798
- const withPremisesAndContext = mergeStrategyCandidates(queryCandidates, premiseCands, contextCands);
826
+ const withPremisesAndContext = mergeStrategyCandidates(queryCandidates, premiseCands, contextCands, contextSimCands);
799
827
  if (premiseCands.length > 0) {
800
828
  traceEntries.push({ node: "strategy", detail: `premise-to-premise → ${premiseCands.length} candidate(s)` });
801
829
  }
802
830
  if (contextCands.length > 0) {
803
831
  traceEntries.push({ node: "strategy", detail: `context-to-intent → ${contextCands.length} candidate(s)` });
804
832
  }
833
+ if (contextSimCands.length > 0) {
834
+ traceEntries.push({ node: "strategy", detail: `context-to-context → ${contextSimCands.length} candidate(s)` });
835
+ }
805
836
  return { candidates: filterByTarget(withPremisesAndContext), trace: traceEntries };
806
837
  }
807
838
  // No search query — premise-to-premise + context-to-intent discovery
808
- const [premiseCands, contextCands] = await Promise.all([
839
+ const [premiseCands, contextCands, contextSimCands] = await Promise.all([
809
840
  runPremiseDiscovery(),
810
841
  runContextToIntentDiscovery(),
842
+ runContextToContextDiscovery(),
811
843
  ]);
812
- if (premiseCands.length > 0 || contextCands.length > 0) {
813
- const merged = mergeStrategyCandidates(premiseCands, contextCands);
844
+ if (premiseCands.length > 0 || contextCands.length > 0 || contextSimCands.length > 0) {
845
+ const merged = mergeStrategyCandidates(premiseCands, contextCands, contextSimCands);
814
846
  const traceEntries = [];
815
847
  if (premiseCands.length > 0) {
816
848
  traceEntries.push({ node: "strategy", detail: `premise-to-premise → ${premiseCands.length} candidate(s)` });
@@ -818,9 +850,12 @@ export class OpportunityGraphFactory {
818
850
  if (contextCands.length > 0) {
819
851
  traceEntries.push({ node: "strategy", detail: `context-to-intent → ${contextCands.length} candidate(s)` });
820
852
  }
853
+ if (contextSimCands.length > 0) {
854
+ traceEntries.push({ node: "strategy", detail: `context-to-context → ${contextSimCands.length} candidate(s)` });
855
+ }
821
856
  traceEntries.push({
822
857
  node: "discovery",
823
- detail: `${[premiseCands.length > 0 && 'premise-to-premise', contextCands.length > 0 && 'context-to-intent'].filter(Boolean).length} strategies → ${premiseCands.length + contextCands.length} raw, ${merged.length} after dedup`,
858
+ detail: `${[premiseCands.length > 0 && 'premise-to-premise', contextCands.length > 0 && 'context-to-intent', contextSimCands.length > 0 && 'context-to-context'].filter(Boolean).length} strategies → ${premiseCands.length + contextCands.length + contextSimCands.length} raw, ${merged.length} after dedup`,
824
859
  });
825
860
  return { candidates: filterByTarget(merged), trace: traceEntries };
826
861
  }
@@ -871,31 +906,47 @@ export class OpportunityGraphFactory {
871
906
  limitPerStrategy,
872
907
  limit: perIndexLimit,
873
908
  minScore,
909
+ corpusGating,
874
910
  });
875
- for (const r of results.filter((x) => x.type === 'intent')) {
876
- all.push(withCandidateEvidence({
877
- candidateUserId: r.userId,
878
- candidateIntentId: r.id,
879
- networkId: targetIndex.networkId,
880
- similarity: r.score,
881
- lens: r.matchedVia,
882
- candidatePayload: '',
883
- candidateSummary: undefined,
884
- discoverySource: 'query',
885
- }));
886
- }
887
- for (const r of results.filter((x) => x.type === 'premise')) {
888
- all.push(withCandidateEvidence({
889
- candidateUserId: r.userId,
890
- candidatePremiseId: r.id,
891
- networkId: targetIndex.networkId,
892
- similarity: r.score,
893
- lens: r.matchedVia,
894
- candidatePayload: '',
895
- candidateSummary: undefined,
896
- discoverySource: 'query',
897
- }));
898
- }
911
+ if (intentResultsEnabled)
912
+ for (const r of results.filter((x) => x.type === 'intent')) {
913
+ all.push(withCandidateEvidence({
914
+ candidateUserId: r.userId,
915
+ candidateIntentId: r.id,
916
+ networkId: targetIndex.networkId,
917
+ similarity: r.score,
918
+ lens: r.matchedVia,
919
+ candidatePayload: '',
920
+ candidateSummary: undefined,
921
+ discoverySource: 'query',
922
+ }));
923
+ }
924
+ if (premiseResultsEnabled)
925
+ for (const r of results.filter((x) => x.type === 'premise')) {
926
+ all.push(withCandidateEvidence({
927
+ candidateUserId: r.userId,
928
+ candidatePremiseId: r.id,
929
+ networkId: targetIndex.networkId,
930
+ similarity: r.score,
931
+ lens: r.matchedVia,
932
+ candidatePayload: '',
933
+ candidateSummary: undefined,
934
+ discoverySource: 'query',
935
+ }));
936
+ }
937
+ if (contextResultsEnabled)
938
+ for (const r of results.filter((x) => x.type === 'user_context')) {
939
+ all.push(withCandidateEvidence({
940
+ candidateUserId: r.userId,
941
+ candidateContextId: r.id,
942
+ networkId: targetIndex.networkId,
943
+ similarity: r.score,
944
+ lens: r.matchedVia,
945
+ candidatePayload: r.text ?? '',
946
+ candidateSummary: undefined,
947
+ discoverySource: 'query',
948
+ }));
949
+ }
899
950
  }));
900
951
  const intentCount = all.filter((c) => c.candidateIntentId).length;
901
952
  const premiseCount = all.filter((c) => c.candidatePremiseId).length;
@@ -903,12 +954,17 @@ export class OpportunityGraphFactory {
903
954
  total: all.length,
904
955
  fromIntent: intentCount,
905
956
  fromPremise: premiseCount,
957
+ fromContext: all.filter((c) => c.candidateContextId).length,
906
958
  });
907
959
  const byKey = new Map();
908
960
  for (const c of all) {
909
961
  // Dedup by candidateUserId + entity (intent or premise), NOT by indexId.
910
962
  // Including indexId caused the same user to appear once per index they belong to.
911
- const entityKey = c.candidateIntentId ? `intent:${c.candidateIntentId}` : `premise:${c.candidatePremiseId}`;
963
+ const entityKey = c.candidateIntentId
964
+ ? `intent:${c.candidateIntentId}`
965
+ : c.candidatePremiseId
966
+ ? `premise:${c.candidatePremiseId}`
967
+ : `context:${c.candidateContextId}`;
912
968
  const key = `${c.candidateUserId}:${entityKey}`;
913
969
  if (!byKey.has(key) || c.similarity > (byKey.get(key)?.similarity ?? 0)) {
914
970
  byKey.set(key, c);
@@ -925,6 +981,10 @@ export class OpportunityGraphFactory {
925
981
  const targetNetworkIds = state.targetNetworks.map(t => t.networkId);
926
982
  if (targetNetworkIds.length === 0)
927
983
  return [];
984
+ if (!discoveryProfileMatchingEnabled() || discoveryProfileSource() === 'user_context') {
985
+ discoveryLog.verbose('runPremiseDiscovery disabled by DISCOVERY_ALLOWED_TYPES/DISCOVERY_PROFILE_SOURCE');
986
+ return [];
987
+ }
928
988
  const sourceLimit = getSourcePremiseDiscoveryLimit();
929
989
  if (sourceLimit === 0) {
930
990
  discoveryLog.verbose('runPremiseDiscovery disabled by DISCOVERY_SOURCE_PREMISE_LIMIT=0');
@@ -1004,6 +1064,8 @@ export class OpportunityGraphFactory {
1004
1064
  const contextToIntentEnabled = process.env.DISCOVERY_CONTEXT_TO_INTENT !== '0';
1005
1065
  if (!contextToIntentEnabled)
1006
1066
  return [];
1067
+ if (!discoveryProfileMatchingEnabled() || discoveryProfileSource() !== 'user_context' || !discoveryIntentMatchingEnabled())
1068
+ return [];
1007
1069
  const targetNetworkIds = state.targetNetworks.map(t => t.networkId);
1008
1070
  if (targetNetworkIds.length === 0)
1009
1071
  return [];
@@ -1031,6 +1093,7 @@ export class OpportunityGraphFactory {
1031
1093
  limitPerStrategy: limitPerStrategy,
1032
1094
  limit: 20,
1033
1095
  minScore,
1096
+ corpusGating,
1034
1097
  });
1035
1098
  for (const r of results.filter(r => r.type === 'intent')) {
1036
1099
  contextCandidates.push(withCandidateEvidence({
@@ -1084,6 +1147,60 @@ export class OpportunityGraphFactory {
1084
1147
  });
1085
1148
  return deduped;
1086
1149
  }
1150
+ /**
1151
+ * Context-to-context discovery (lightweight profile mode only).
1152
+ * Raw network-scoped context embeddings → candidate user_contexts.
1153
+ * No HyDE: both sides are the same document type (synthesized
1154
+ * 3–6 sentence paragraphs), so direct paragraph similarity is correct
1155
+ * (same rationale as raw premise-to-premise).
1156
+ */
1157
+ async function runContextToContextDiscovery() {
1158
+ if (!contextResultsEnabled)
1159
+ return [];
1160
+ if (!state.sourceContexts?.length)
1161
+ return [];
1162
+ if (typeof self.database.searchUserContextsBySimilarity !== 'function')
1163
+ return [];
1164
+ const targetNetworkIds = state.targetNetworks.map(t => t.networkId);
1165
+ if (targetNetworkIds.length === 0)
1166
+ return [];
1167
+ const contextCandidates = [];
1168
+ for (const ctx of state.sourceContexts.filter(c => targetNetworkIds.includes(c.networkId))) {
1169
+ const results = await self.database.searchUserContextsBySimilarity({
1170
+ embedding: ctx.embedding,
1171
+ networkIds: [ctx.networkId],
1172
+ excludeUserId: discoveryUserId,
1173
+ limit: 20,
1174
+ minScore,
1175
+ });
1176
+ for (const r of results) {
1177
+ contextCandidates.push(withCandidateEvidence({
1178
+ candidateUserId: r.userId,
1179
+ candidateContextId: r.contextId,
1180
+ sourceContextId: ctx.contextId,
1181
+ networkId: r.networkId,
1182
+ similarity: typeof r.similarity === 'number' ? r.similarity : parseFloat(String(r.similarity)),
1183
+ lens: 'context_match',
1184
+ candidatePayload: r.text ?? '',
1185
+ discoverySource: 'context-similarity',
1186
+ }));
1187
+ }
1188
+ }
1189
+ // Dedup by userId + contextId + networkId (mirror premise dedup)
1190
+ const byKey = new Map();
1191
+ for (const c of contextCandidates) {
1192
+ const key = `${c.candidateUserId}:${c.candidateContextId ?? 'none'}:${c.networkId}`;
1193
+ if (!byKey.has(key) || c.similarity > (byKey.get(key)?.similarity ?? 0)) {
1194
+ byKey.set(key, c);
1195
+ }
1196
+ }
1197
+ const deduped = Array.from(byKey.values());
1198
+ discoveryLog.verbose('runContextToContextDiscovery complete', {
1199
+ rawCount: contextCandidates.length,
1200
+ dedupedCount: deduped.length,
1201
+ });
1202
+ return deduped;
1203
+ }
1087
1204
  /**
1088
1205
  * Merge candidates from multiple strategies. Deduplicates by userId + networkId + entityId,
1089
1206
  * keeps the highest similarity, tracks which strategies found each candidate,
@@ -1094,7 +1211,7 @@ export class OpportunityGraphFactory {
1094
1211
  const merged = new Map();
1095
1212
  for (const group of groups) {
1096
1213
  for (const c of group) {
1097
- const entityId = c.candidateIntentId ?? c.candidatePremiseId ?? 'none';
1214
+ const entityId = c.candidateIntentId ?? c.candidatePremiseId ?? c.candidateContextId ?? 'none';
1098
1215
  const key = `${c.candidateUserId}:${c.networkId}:${entityId}`;
1099
1216
  const existing = merged.get(key);
1100
1217
  if (!existing) {
@@ -1129,15 +1246,16 @@ export class OpportunityGraphFactory {
1129
1246
  const searchText = state.searchQuery ?? resolvedIntent?.payload ?? '';
1130
1247
  if (!searchText) {
1131
1248
  discoveryLog.warn('No search text available for intent path');
1132
- const [premiseCands, contextCands] = await Promise.all([
1249
+ const [premiseCands, contextCands, contextSimCands] = await Promise.all([
1133
1250
  runPremiseDiscovery(),
1134
1251
  runContextToIntentDiscovery(),
1252
+ runContextToContextDiscovery(),
1135
1253
  ]);
1136
- const merged = mergeStrategyCandidates(premiseCands, contextCands);
1254
+ const merged = mergeStrategyCandidates(premiseCands, contextCands, contextSimCands);
1137
1255
  if (merged.length > 0) {
1138
1256
  return {
1139
1257
  candidates: filterByTarget(merged),
1140
- trace: [{ node: "discovery", detail: `No search text; premise → ${premiseCands.length}, context → ${contextCands.length}, merged → ${merged.length} candidate(s)` }],
1258
+ trace: [{ node: "discovery", detail: `No search text; premise → ${premiseCands.length}, context → ${contextCands.length}, context-sim → ${contextSimCands.length}, merged → ${merged.length} candidate(s)` }],
1141
1259
  };
1142
1260
  }
1143
1261
  return { candidates: [] };
@@ -1156,16 +1274,17 @@ export class OpportunityGraphFactory {
1156
1274
  const hydeEmbeddings = hydeResult.hydeEmbeddings;
1157
1275
  const lenses = hydeResult.lenses ?? [];
1158
1276
  if (!hydeEmbeddings || Object.keys(hydeEmbeddings).length === 0) {
1159
- const [premiseCands, contextCands] = await Promise.all([
1277
+ const [premiseCands, contextCands, contextSimCands] = await Promise.all([
1160
1278
  runPremiseDiscovery(),
1161
1279
  runContextToIntentDiscovery(),
1280
+ runContextToContextDiscovery(),
1162
1281
  ]);
1163
- const merged = mergeStrategyCandidates(premiseCands, contextCands);
1282
+ const merged = mergeStrategyCandidates(premiseCands, contextCands, contextSimCands);
1164
1283
  if (merged.length > 0) {
1165
1284
  return {
1166
1285
  hydeEmbeddings: {},
1167
1286
  candidates: filterByTarget(merged),
1168
- trace: [{ node: "discovery", detail: `No HyDE embeddings; premise → ${premiseCands.length}, context → ${contextCands.length}, merged → ${merged.length} candidate(s)` }],
1287
+ trace: [{ node: "discovery", detail: `No HyDE embeddings; premise → ${premiseCands.length}, context → ${contextCands.length}, context-sim → ${contextSimCands.length}, merged → ${merged.length} candidate(s)` }],
1169
1288
  };
1170
1289
  }
1171
1290
  return { hydeEmbeddings: {}, candidates: [] };
@@ -1186,35 +1305,55 @@ export class OpportunityGraphFactory {
1186
1305
  limitPerStrategy,
1187
1306
  limit: perIndexLimit,
1188
1307
  minScore,
1308
+ corpusGating,
1189
1309
  });
1190
- for (const result of results.filter((r) => r.type === 'intent')) {
1191
- allCandidates.push(withCandidateEvidence({
1192
- candidateUserId: result.userId,
1193
- candidateIntentId: result.id,
1194
- networkId: targetIndex.networkId,
1195
- similarity: result.score,
1196
- lens: result.matchedVia,
1197
- candidatePayload: '',
1198
- candidateSummary: undefined,
1199
- discoverySource: 'query',
1200
- }));
1201
- }
1202
- for (const result of results.filter((r) => r.type === 'premise')) {
1203
- allCandidates.push(withCandidateEvidence({
1204
- candidateUserId: result.userId,
1205
- candidatePremiseId: result.id,
1206
- networkId: targetIndex.networkId,
1207
- similarity: result.score,
1208
- lens: result.matchedVia,
1209
- candidatePayload: '',
1210
- candidateSummary: undefined,
1211
- discoverySource: 'query',
1212
- }));
1213
- }
1310
+ if (intentResultsEnabled)
1311
+ for (const result of results.filter((r) => r.type === 'intent')) {
1312
+ allCandidates.push(withCandidateEvidence({
1313
+ candidateUserId: result.userId,
1314
+ candidateIntentId: result.id,
1315
+ networkId: targetIndex.networkId,
1316
+ similarity: result.score,
1317
+ lens: result.matchedVia,
1318
+ candidatePayload: '',
1319
+ candidateSummary: undefined,
1320
+ discoverySource: 'query',
1321
+ }));
1322
+ }
1323
+ if (premiseResultsEnabled)
1324
+ for (const result of results.filter((r) => r.type === 'premise')) {
1325
+ allCandidates.push(withCandidateEvidence({
1326
+ candidateUserId: result.userId,
1327
+ candidatePremiseId: result.id,
1328
+ networkId: targetIndex.networkId,
1329
+ similarity: result.score,
1330
+ lens: result.matchedVia,
1331
+ candidatePayload: '',
1332
+ candidateSummary: undefined,
1333
+ discoverySource: 'query',
1334
+ }));
1335
+ }
1336
+ if (contextResultsEnabled)
1337
+ for (const result of results.filter((r) => r.type === 'user_context')) {
1338
+ allCandidates.push(withCandidateEvidence({
1339
+ candidateUserId: result.userId,
1340
+ candidateContextId: result.id,
1341
+ networkId: targetIndex.networkId,
1342
+ similarity: result.score,
1343
+ lens: result.matchedVia,
1344
+ candidatePayload: result.text ?? '',
1345
+ candidateSummary: undefined,
1346
+ discoverySource: 'query',
1347
+ }));
1348
+ }
1214
1349
  }));
1215
1350
  const byUserAndIndex = new Map();
1216
1351
  for (const c of allCandidates) {
1217
- const entityKey = c.candidateIntentId ? `intent:${c.candidateIntentId}` : `premise:${c.candidatePremiseId}`;
1352
+ const entityKey = c.candidateIntentId
1353
+ ? `intent:${c.candidateIntentId}`
1354
+ : c.candidatePremiseId
1355
+ ? `premise:${c.candidatePremiseId}`
1356
+ : `context:${c.candidateContextId}`;
1218
1357
  const key = `${c.candidateUserId}:${c.networkId}:${entityKey}`;
1219
1358
  if (!byUserAndIndex.has(key) || c.similarity > (byUserAndIndex.get(key)?.similarity ?? 0)) {
1220
1359
  byUserAndIndex.set(key, c);
@@ -1295,15 +1434,16 @@ export class OpportunityGraphFactory {
1295
1434
  },
1296
1435
  });
1297
1436
  }
1298
- const [premiseCands, contextCands] = await Promise.all([
1437
+ const [premiseCands, contextCands, contextSimCands] = await Promise.all([
1299
1438
  runPremiseDiscovery(),
1300
1439
  runContextToIntentDiscovery(),
1440
+ runContextToContextDiscovery(),
1301
1441
  ]);
1302
- const allStrategies = mergeStrategyCandidates(candidates, premiseCands, contextCands);
1303
- if (premiseCands.length > 0 || contextCands.length > 0) {
1442
+ const allStrategies = mergeStrategyCandidates(candidates, premiseCands, contextCands, contextSimCands);
1443
+ if (premiseCands.length > 0 || contextCands.length > 0 || contextSimCands.length > 0) {
1304
1444
  traceEntries.push({
1305
1445
  node: "discovery",
1306
- detail: `+ Premise → ${premiseCands.length}, Context → ${contextCands.length}, merged to ${allStrategies.length} candidate(s)`,
1446
+ detail: `+ Premise → ${premiseCands.length}, Context → ${contextCands.length}, Context-sim → ${contextSimCands.length}, merged to ${allStrategies.length} candidate(s)`,
1307
1447
  });
1308
1448
  }
1309
1449
  return {
@@ -1437,7 +1577,7 @@ export class OpportunityGraphFactory {
1437
1577
  }
1438
1578
  catch (err) {
1439
1579
  evaluationLog.warn('IND-567 rejection cool-down: lookup failed, skipping penalty', {
1440
- error: err instanceof Error ? err.message : String(err),
1580
+ error: safeOpportunityGraphError(err),
1441
1581
  });
1442
1582
  }
1443
1583
  }
@@ -1493,6 +1633,7 @@ export class OpportunityGraphFactory {
1493
1633
  const profile = await this.database.getProfile(c.candidateUserId);
1494
1634
  let intentPayload = c.candidatePayload;
1495
1635
  let intentSummary = c.candidateSummary;
1636
+ let evidence = c.evidence;
1496
1637
  if (c.candidateIntentId != null && (!intentPayload || intentPayload === '')) {
1497
1638
  const intent = await this.database.getIntent(c.candidateIntentId);
1498
1639
  if (intent) {
@@ -1500,6 +1641,16 @@ export class OpportunityGraphFactory {
1500
1641
  intentSummary = intent.summary ?? undefined;
1501
1642
  }
1502
1643
  }
1644
+ if (c.candidatePremiseId != null && (!intentPayload || intentPayload === '')) {
1645
+ const premise = await this.database.getPremise(c.candidatePremiseId);
1646
+ if (premise) {
1647
+ intentPayload = premise.assertion.text;
1648
+ intentSummary = premise.assertion.summary;
1649
+ evidence = (c.evidence ?? []).map((item) => item.candidatePremiseId === c.candidatePremiseId
1650
+ ? { ...item, payload: premise.assertion.text, summary: premise.assertion.summary, assertionText: premise.assertion.text }
1651
+ : item);
1652
+ }
1653
+ }
1503
1654
  const evidenceKey = buildEvaluatorEvidenceKey(c);
1504
1655
  return {
1505
1656
  userId: c.candidateUserId,
@@ -1516,7 +1667,7 @@ export class OpportunityGraphFactory {
1516
1667
  evidenceKey,
1517
1668
  ragScore: c.similarity * 100,
1518
1669
  matchedVia: c.lens,
1519
- evidence: c.evidence,
1670
+ evidence,
1520
1671
  };
1521
1672
  }));
1522
1673
  const userIdToIndexId = new Map();
@@ -1545,6 +1696,7 @@ export class OpportunityGraphFactory {
1545
1696
  }
1546
1697
  // Lower default threshold to 50 for better recall
1547
1698
  const minScore = state.options.minScore ?? 50;
1699
+ const evaluatorSignalConfig = getAbortSignalConfig();
1548
1700
  const evaluator = typeof evaluatorAgent.invokeEntityBundle === 'function'
1549
1701
  ? evaluatorAgent
1550
1702
  : new OpportunityEvaluator();
@@ -1569,7 +1721,7 @@ export class OpportunityGraphFactory {
1569
1721
  const _traceEmitter = requestContext.getStore()?.traceEmitter;
1570
1722
  _traceEmitter?.({ type: "agent_start", name: "opportunity-evaluator" });
1571
1723
  const _candidateName = candidateEntity.profile?.name ?? "Unknown";
1572
- return evaluator.invokeEntityBundle(input, { minScore, returnAll: true })
1724
+ return evaluator.invokeEntityBundle(input, { minScore, returnAll: true, ...evaluatorSignalConfig })
1573
1725
  .then((res) => {
1574
1726
  const _evalDuration = Date.now() - _evalStart;
1575
1727
  agentTimingsAccum.push({ name: 'opportunity.evaluator', durationMs: _evalDuration });
@@ -1580,12 +1732,12 @@ export class OpportunityGraphFactory {
1580
1732
  })
1581
1733
  .catch((err) => {
1582
1734
  const _evalDuration = Date.now() - _evalStart;
1583
- const _errMsg = err instanceof Error ? err.message : String(err);
1735
+ const _errMsg = safeOpportunityGraphError(err);
1584
1736
  agentTimingsAccum.push({ name: 'opportunity.evaluator', durationMs: _evalDuration });
1585
1737
  _traceEmitter?.({ type: "agent_end", name: "opportunity-evaluator", durationMs: _evalDuration, summary: `${_candidateName}: error — ${_errMsg}` });
1586
1738
  evaluationLog.warn('Parallel eval failed for candidate', {
1587
1739
  candidateUserId: candidateEntity.userId,
1588
- error: err,
1740
+ error: _errMsg,
1589
1741
  });
1590
1742
  parallelErrors.push({
1591
1743
  candidateUserId: candidateEntity.userId,
@@ -1632,14 +1784,14 @@ export class OpportunityGraphFactory {
1632
1784
  _traceEmitterSerial?.({ type: "agent_start", name: "opportunity-evaluator" });
1633
1785
  let opportunitiesWithActors;
1634
1786
  try {
1635
- opportunitiesWithActors = await evaluator.invokeEntityBundle(input, { minScore, returnAll: true });
1787
+ opportunitiesWithActors = await evaluator.invokeEntityBundle(input, { minScore, returnAll: true, ...evaluatorSignalConfig });
1636
1788
  const _evalDuration = Date.now() - _evalStart;
1637
1789
  agentTimingsAccum.push({ name: 'opportunity.evaluator', durationMs: _evalDuration });
1638
1790
  _traceEmitterSerial?.({ type: "agent_end", name: "opportunity-evaluator", durationMs: _evalDuration, summary: `Evaluated ${candidateEntities.length} candidate(s)` });
1639
1791
  }
1640
1792
  catch (serialErr) {
1641
1793
  const _evalDuration = Date.now() - _evalStart;
1642
- const _errMsg = serialErr instanceof Error ? serialErr.message : String(serialErr);
1794
+ const _errMsg = safeOpportunityGraphError(serialErr);
1643
1795
  agentTimingsAccum.push({ name: 'opportunity.evaluator', durationMs: _evalDuration });
1644
1796
  _traceEmitterSerial?.({ type: "agent_end", name: "opportunity-evaluator", durationMs: _evalDuration, summary: `error — ${_errMsg}` });
1645
1797
  throw serialErr; // Re-throw for the outer catch to handle
@@ -1816,8 +1968,8 @@ export class OpportunityGraphFactory {
1816
1968
  };
1817
1969
  }
1818
1970
  catch (error) {
1819
- const errMsg = error instanceof Error ? error.message : String(error);
1820
- evaluationLog.error('Failed', { error });
1971
+ const errMsg = safeOpportunityGraphError(error);
1972
+ evaluationLog.error('Failed', { error: errMsg });
1821
1973
  return {
1822
1974
  evaluatedOpportunities: [],
1823
1975
  error: 'Failed to evaluate candidates.',
@@ -2391,7 +2543,7 @@ export class OpportunityGraphFactory {
2391
2543
  _evalStart = Date.now();
2392
2544
  _traceEmitterIntro?.({ type: "agent_start", name: "intro-evaluator" });
2393
2545
  _introEvalStarted = true;
2394
- const evaluated = await evaluatorAgent.invokeEntityBundle(input, { minScore: 0 });
2546
+ const evaluated = await evaluatorAgent.invokeEntityBundle(input, { minScore: 0, ...getAbortSignalConfig() });
2395
2547
  const _introDuration = Date.now() - _evalStart;
2396
2548
  agentTimingsAccum.push({ name: 'opportunity.evaluator', durationMs: _introDuration });
2397
2549
  _traceEmitterIntro?.({ type: "agent_end", name: "intro-evaluator", durationMs: _introDuration, summary: "Evaluated introduction" });
@@ -0,0 +1,14 @@
1
+ /** Data types allowed to participate in opportunity matching. */
2
+ export type DiscoveryMatchType = 'intent' | 'profile';
3
+ /** Test hook: clear warn-once state so each test observes warnings. */
4
+ export declare function resetDiscoveryEnvWarningsForTests(): void;
5
+ /** Current DISCOVERY_ALLOWED_TYPES set (default: intent + profile). */
6
+ export declare function discoveryAllowedTypes(): ReadonlySet<DiscoveryMatchType>;
7
+ /** True when intents may participate in matching (source or candidate). */
8
+ export declare function discoveryIntentMatchingEnabled(): boolean;
9
+ /** True when profile data (premises or user_contexts) may participate. */
10
+ export declare function discoveryProfileMatchingEnabled(): boolean;
11
+ /** What "profile" means in matching: heavyweight premises or lightweight user_contexts. */
12
+ export type DiscoveryProfileSource = 'premise' | 'user_context';
13
+ /** Current DISCOVERY_PROFILE_SOURCE (default: premise). */
14
+ export declare function discoveryProfileSource(): DiscoveryProfileSource;