@indexnetwork/protocol 4.15.0-rc.357.1 → 4.16.1-rc.359.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 +6 -0
- package/dist/contact/contact.inviter.d.ts.map +1 -1
- package/dist/contact/contact.inviter.js +12 -5
- package/dist/contact/contact.inviter.js.map +1 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/opportunity/delivery-card.cache.d.ts.map +1 -1
- package/dist/opportunity/delivery-card.cache.js +7 -3
- package/dist/opportunity/delivery-card.cache.js.map +1 -1
- package/dist/opportunity/discriminator/discriminator.adjustments.d.ts +22 -15
- package/dist/opportunity/discriminator/discriminator.adjustments.d.ts.map +1 -1
- package/dist/opportunity/discriminator/discriminator.adjustments.js +39 -20
- package/dist/opportunity/discriminator/discriminator.adjustments.js.map +1 -1
- package/dist/opportunity/feed/feed.categorizer.d.ts.map +1 -1
- package/dist/opportunity/feed/feed.categorizer.js +6 -0
- package/dist/opportunity/feed/feed.categorizer.js.map +1 -1
- package/dist/opportunity/feed/feed.graph.d.ts +2 -0
- package/dist/opportunity/feed/feed.graph.d.ts.map +1 -1
- package/dist/opportunity/feed/feed.graph.js +53 -28
- package/dist/opportunity/feed/feed.graph.js.map +1 -1
- package/dist/opportunity/feed/feed.state.d.ts +3 -1
- package/dist/opportunity/feed/feed.state.d.ts.map +1 -1
- package/dist/opportunity/feed/feed.state.js.map +1 -1
- package/dist/opportunity/opportunity.claim-safety.d.ts +25 -0
- package/dist/opportunity/opportunity.claim-safety.d.ts.map +1 -0
- package/dist/opportunity/opportunity.claim-safety.js +89 -0
- package/dist/opportunity/opportunity.claim-safety.js.map +1 -0
- package/dist/opportunity/opportunity.discover.d.ts.map +1 -1
- package/dist/opportunity/opportunity.discover.js +6 -1
- package/dist/opportunity/opportunity.discover.js.map +1 -1
- package/dist/opportunity/opportunity.evaluator.d.ts.map +1 -1
- package/dist/opportunity/opportunity.evaluator.js +13 -8
- package/dist/opportunity/opportunity.evaluator.js.map +1 -1
- package/dist/opportunity/opportunity.graph.d.ts +36 -0
- package/dist/opportunity/opportunity.graph.d.ts.map +1 -1
- package/dist/opportunity/opportunity.graph.js +177 -18
- package/dist/opportunity/opportunity.graph.js.map +1 -1
- package/dist/opportunity/opportunity.persist.d.ts +8 -1
- package/dist/opportunity/opportunity.persist.d.ts.map +1 -1
- package/dist/opportunity/opportunity.persist.js +23 -2
- package/dist/opportunity/opportunity.persist.js.map +1 -1
- package/dist/opportunity/opportunity.presentation-cache.d.ts +7 -0
- package/dist/opportunity/opportunity.presentation-cache.d.ts.map +1 -0
- package/dist/opportunity/opportunity.presentation-cache.js +15 -0
- package/dist/opportunity/opportunity.presentation-cache.js.map +1 -0
- package/dist/opportunity/opportunity.presentation.d.ts.map +1 -1
- package/dist/opportunity/opportunity.presentation.js +8 -5
- package/dist/opportunity/opportunity.presentation.js.map +1 -1
- package/dist/opportunity/opportunity.presenter.d.ts +4 -4
- package/dist/opportunity/opportunity.presenter.d.ts.map +1 -1
- package/dist/opportunity/opportunity.presenter.js +55 -14
- package/dist/opportunity/opportunity.presenter.js.map +1 -1
- package/dist/opportunity/opportunity.safe-presentation.d.ts +1 -1
- package/dist/opportunity/opportunity.safe-presentation.d.ts.map +1 -1
- package/dist/opportunity/opportunity.safe-presentation.js +22 -9
- package/dist/opportunity/opportunity.safe-presentation.js.map +1 -1
- package/dist/opportunity/opportunity.state.d.ts +3 -0
- package/dist/opportunity/opportunity.state.d.ts.map +1 -1
- package/dist/opportunity/opportunity.state.js.map +1 -1
- package/dist/opportunity/opportunity.tools.d.ts +1 -17
- package/dist/opportunity/opportunity.tools.d.ts.map +1 -1
- package/dist/opportunity/opportunity.tools.js +32 -2
- package/dist/opportunity/opportunity.tools.js.map +1 -1
- package/dist/shared/agent/model.config.d.ts +5 -0
- package/dist/shared/agent/model.config.d.ts.map +1 -1
- package/dist/shared/agent/model.config.js +1 -0
- package/dist/shared/agent/model.config.js.map +1 -1
- package/dist/shared/hyde/hyde.documents.d.ts +10 -0
- package/dist/shared/hyde/hyde.documents.d.ts.map +1 -0
- package/dist/shared/hyde/hyde.documents.js +50 -0
- package/dist/shared/hyde/hyde.documents.js.map +1 -0
- package/dist/shared/hyde/hyde.env.d.ts +9 -0
- package/dist/shared/hyde/hyde.env.d.ts.map +1 -0
- package/dist/shared/hyde/hyde.env.js +10 -0
- package/dist/shared/hyde/hyde.env.js.map +1 -0
- package/dist/shared/hyde/hyde.frame.d.ts +151 -0
- package/dist/shared/hyde/hyde.frame.d.ts.map +1 -0
- package/dist/shared/hyde/hyde.frame.js +108 -0
- package/dist/shared/hyde/hyde.frame.js.map +1 -0
- package/dist/shared/hyde/hyde.generator.d.ts +23 -11
- package/dist/shared/hyde/hyde.generator.d.ts.map +1 -1
- package/dist/shared/hyde/hyde.generator.js +50 -21
- package/dist/shared/hyde/hyde.generator.js.map +1 -1
- package/dist/shared/hyde/hyde.graph.d.ts +58 -17
- package/dist/shared/hyde/hyde.graph.d.ts.map +1 -1
- package/dist/shared/hyde/hyde.graph.js +333 -89
- package/dist/shared/hyde/hyde.graph.js.map +1 -1
- package/dist/shared/hyde/hyde.state.d.ts +19 -2
- package/dist/shared/hyde/hyde.state.d.ts.map +1 -1
- package/dist/shared/hyde/hyde.state.js +23 -3
- package/dist/shared/hyde/hyde.state.js.map +1 -1
- package/dist/shared/hyde/hyde.validator.d.ts +79 -0
- package/dist/shared/hyde/hyde.validator.d.ts.map +1 -0
- package/dist/shared/hyde/hyde.validator.js +49 -0
- package/dist/shared/hyde/hyde.validator.js.map +1 -0
- package/dist/shared/hyde/lens.inferrer.d.ts +190 -12
- package/dist/shared/hyde/lens.inferrer.d.ts.map +1 -1
- package/dist/shared/hyde/lens.inferrer.js +93 -38
- package/dist/shared/hyde/lens.inferrer.js.map +1 -1
- package/dist/shared/interfaces/database.interface.d.ts +46 -2
- package/dist/shared/interfaces/database.interface.d.ts.map +1 -1
- package/dist/shared/interfaces/database.interface.js.map +1 -1
- package/package.json +2 -1
|
@@ -17,8 +17,11 @@ import { resolveInitialStatus } from './opportunity.state.js';
|
|
|
17
17
|
import { OpportunityEvaluator } from './opportunity.evaluator.js';
|
|
18
18
|
import { IntentIndexer } from '../intent/intent.indexer.js';
|
|
19
19
|
import { getModelName } from '../shared/agent/model.config.js';
|
|
20
|
+
import { selectHydeDocumentsForGeneration } from '../shared/hyde/hyde.documents.js';
|
|
21
|
+
import { getHydeGenerationMode } from '../shared/hyde/hyde.env.js';
|
|
20
22
|
import { validateOpportunityActors } from './opportunity.utils.js';
|
|
21
|
-
import {
|
|
23
|
+
import { safeFallbackSummary } from './opportunity.safe-presentation.js';
|
|
24
|
+
import { hasUnsupportedOpportunityClaim } from './opportunity.claim-safety.js';
|
|
22
25
|
import { persistOpportunities } from './opportunity.persist.js';
|
|
23
26
|
import { INTRODUCER_DISCOVERY_SOURCE } from './opportunity.introducer.js';
|
|
24
27
|
import { negotiateCandidates } from "../negotiation/negotiation.graph.js";
|
|
@@ -52,6 +55,9 @@ const routingLog = protocolLogger('OpportunityGraph:Routing');
|
|
|
52
55
|
const DEDUP_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
|
|
53
56
|
/** Default cap for source premises used by premise-to-premise discovery. Prevents BACKEND-5-style fan-out. */
|
|
54
57
|
const DEFAULT_SOURCE_PREMISE_DISCOVERY_LIMIT = 40;
|
|
58
|
+
function networkMembershipPairKey(userId, networkId) {
|
|
59
|
+
return `${userId}\u0000${networkId}`;
|
|
60
|
+
}
|
|
55
61
|
/** Per-source cap for candidate premise matches. */
|
|
56
62
|
const PREMISE_MATCH_LIMIT_PER_SOURCE = 20;
|
|
57
63
|
/** Resolve the source premise discovery cap from env, preserving 0 as an explicit disable switch. */
|
|
@@ -273,6 +279,7 @@ export class OpportunityGraphFactory {
|
|
|
273
279
|
.map((c) => ({
|
|
274
280
|
contextId: c.id,
|
|
275
281
|
networkId: c.networkId,
|
|
282
|
+
text: c.text,
|
|
276
283
|
embedding: c.embedding,
|
|
277
284
|
}));
|
|
278
285
|
return {
|
|
@@ -334,10 +341,11 @@ export class OpportunityGraphFactory {
|
|
|
334
341
|
}
|
|
335
342
|
targetIndexIds = [state.networkId];
|
|
336
343
|
}
|
|
337
|
-
else if (state.indexScope
|
|
344
|
+
else if (state.indexScope !== undefined) {
|
|
338
345
|
// Bounded scope (e.g. a network-scoped agent's reachable networks):
|
|
339
346
|
// intersect with the user's actual memberships so discovery never
|
|
340
|
-
// reaches networks outside the agent's bound scope.
|
|
347
|
+
// reaches networks outside the agent's bound scope. An explicit
|
|
348
|
+
// empty scope is authoritative and must fail closed.
|
|
341
349
|
const allowed = new Set(state.indexScope);
|
|
342
350
|
targetIndexIds = state.userNetworks.filter((n) => allowed.has(n));
|
|
343
351
|
scopeLog.verbose('Applied indexScope intersection', {
|
|
@@ -350,6 +358,19 @@ export class OpportunityGraphFactory {
|
|
|
350
358
|
// Search all user's indexes
|
|
351
359
|
targetIndexIds = state.userNetworks;
|
|
352
360
|
}
|
|
361
|
+
if (state.triggerIntentId) {
|
|
362
|
+
// A trigger intent is an authoritative discovery boundary, not just
|
|
363
|
+
// ranking context. Recompute the intersection at the graph edge so
|
|
364
|
+
// direct chat/MCP callers cannot bypass FromIntentQueue admission.
|
|
365
|
+
const assignedNetworkIds = new Set(await this.database.getNetworkIdsForIntent(state.triggerIntentId));
|
|
366
|
+
const activeOwnerNetworkIds = new Set(state.userNetworks);
|
|
367
|
+
targetIndexIds = targetIndexIds.filter((networkId) => assignedNetworkIds.has(networkId) && activeOwnerNetworkIds.has(networkId));
|
|
368
|
+
scopeLog.verbose('Applied trigger-intent network intersection', {
|
|
369
|
+
triggerIntentId: state.triggerIntentId,
|
|
370
|
+
assignedCount: assignedNetworkIds.size,
|
|
371
|
+
targetCount: targetIndexIds.length,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
353
374
|
// Fetch index details
|
|
354
375
|
const targetNetworks = await Promise.all(targetIndexIds.map(async (networkId) => {
|
|
355
376
|
const index = await this.database.getNetwork(networkId);
|
|
@@ -483,6 +504,19 @@ export class OpportunityGraphFactory {
|
|
|
483
504
|
try {
|
|
484
505
|
let resolvedIntentId;
|
|
485
506
|
if (state.triggerIntentId) {
|
|
507
|
+
const isOwnedActiveIntent = state.indexedIntents.some((intent) => intent.intentId === state.triggerIntentId);
|
|
508
|
+
if (!isOwnedActiveIntent) {
|
|
509
|
+
resolveLog.warn('Trigger intent is not an active intent owned by the discovery user', {
|
|
510
|
+
triggerIntentId: state.triggerIntentId,
|
|
511
|
+
userId: state.userId,
|
|
512
|
+
});
|
|
513
|
+
return {
|
|
514
|
+
resolvedTriggerIntentId: undefined,
|
|
515
|
+
resolvedIntentInIndex: false,
|
|
516
|
+
discoverySource: 'context',
|
|
517
|
+
error: 'Trigger intent is not available for discovery.',
|
|
518
|
+
};
|
|
519
|
+
}
|
|
486
520
|
const inNetwork = await this.database.getNetworkIdsForIntent(state.triggerIntentId);
|
|
487
521
|
const inTarget = inNetwork.some((id) => targetIndexIds.includes(id));
|
|
488
522
|
resolvedIntentId = state.triggerIntentId;
|
|
@@ -961,7 +995,8 @@ export class OpportunityGraphFactory {
|
|
|
961
995
|
const contextCandidates = [];
|
|
962
996
|
for (const ctx of state.sourceContexts.filter(c => targetNetworkIds.includes(c.networkId))) {
|
|
963
997
|
// Attempt HyDE-enhanced search first
|
|
964
|
-
const
|
|
998
|
+
const persistedHydeDocs = await self.database.getHydeDocumentsForSource('context', ctx.contextId);
|
|
999
|
+
const hydeDocs = selectHydeDocumentsForGeneration(persistedHydeDocs, getHydeGenerationMode(), ctx.text);
|
|
965
1000
|
const lensEmbeddings = hydeDocs
|
|
966
1001
|
.filter(d => d.hydeEmbedding?.length > 0)
|
|
967
1002
|
.map(d => ({
|
|
@@ -1319,6 +1354,38 @@ export class OpportunityGraphFactory {
|
|
|
1319
1354
|
const dedupedCandidates = Array.from(bestByUser.values());
|
|
1320
1355
|
// Re-sort by similarity descending (Map iteration order doesn't guarantee sort)
|
|
1321
1356
|
dedupedCandidates.sort((a, b) => b.similarity - a.similarity);
|
|
1357
|
+
const discoveryUserId = state.onBehalfOfUserId ?? state.userId;
|
|
1358
|
+
let eligibleCandidates;
|
|
1359
|
+
try {
|
|
1360
|
+
const requestedPairs = dedupedCandidates.flatMap((candidate) => [
|
|
1361
|
+
{ userId: discoveryUserId, networkId: candidate.networkId },
|
|
1362
|
+
{ userId: candidate.candidateUserId, networkId: candidate.networkId },
|
|
1363
|
+
]);
|
|
1364
|
+
const activePairs = await this.database.getActiveNetworkMembershipPairs(requestedPairs);
|
|
1365
|
+
const activePairKeys = new Set(activePairs.map((pair) => networkMembershipPairKey(pair.userId, pair.networkId)));
|
|
1366
|
+
eligibleCandidates = dedupedCandidates.filter((candidate) => activePairKeys.has(networkMembershipPairKey(discoveryUserId, candidate.networkId))
|
|
1367
|
+
&& activePairKeys.has(networkMembershipPairKey(candidate.candidateUserId, candidate.networkId)));
|
|
1368
|
+
}
|
|
1369
|
+
catch (error) {
|
|
1370
|
+
evaluationLog.error('Active network membership recheck failed; skipping evaluation', { error });
|
|
1371
|
+
return {
|
|
1372
|
+
candidates: [],
|
|
1373
|
+
evaluatedOpportunities: [],
|
|
1374
|
+
remainingCandidates: [],
|
|
1375
|
+
error: 'Failed to validate candidate network memberships.',
|
|
1376
|
+
agentTimings: [],
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
if (eligibleCandidates.length < dedupedCandidates.length) {
|
|
1380
|
+
evaluationLog.info('Removed candidates without active network pairs before evaluation', {
|
|
1381
|
+
before: dedupedCandidates.length,
|
|
1382
|
+
after: eligibleCandidates.length,
|
|
1383
|
+
removed: dedupedCandidates.length - eligibleCandidates.length,
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
if (eligibleCandidates.length === 0) {
|
|
1387
|
+
return { candidates: [], evaluatedOpportunities: [], remainingCandidates: [], agentTimings: [] };
|
|
1388
|
+
}
|
|
1322
1389
|
if (dedupedCandidates.length < sortedCandidates.length) {
|
|
1323
1390
|
evaluationLog.info("Deduped candidates by userId", {
|
|
1324
1391
|
before: sortedCandidates.length,
|
|
@@ -1326,8 +1393,8 @@ export class OpportunityGraphFactory {
|
|
|
1326
1393
|
removed: sortedCandidates.length - dedupedCandidates.length,
|
|
1327
1394
|
});
|
|
1328
1395
|
}
|
|
1329
|
-
const batchToEvaluate =
|
|
1330
|
-
const remaining =
|
|
1396
|
+
const batchToEvaluate = eligibleCandidates.slice(0, EVAL_BATCH_SIZE);
|
|
1397
|
+
const remaining = eligibleCandidates.slice(EVAL_BATCH_SIZE);
|
|
1331
1398
|
// Early termination: if search was query-driven and no query-sourced candidates remain,
|
|
1332
1399
|
// clear remaining to prevent pointless pagination through non-query leftovers
|
|
1333
1400
|
const isQueryDriven = !!state.searchQuery?.trim();
|
|
@@ -1347,7 +1414,6 @@ export class OpportunityGraphFactory {
|
|
|
1347
1414
|
}
|
|
1348
1415
|
const agentTimingsAccum = [];
|
|
1349
1416
|
try {
|
|
1350
|
-
const discoveryUserId = state.onBehalfOfUserId ?? state.userId;
|
|
1351
1417
|
const sourceProfile = await this.database.getProfile(discoveryUserId);
|
|
1352
1418
|
const sourceEntity = {
|
|
1353
1419
|
userId: discoveryUserId,
|
|
@@ -1686,6 +1752,7 @@ export class OpportunityGraphFactory {
|
|
|
1686
1752
|
// Only pass opportunities that passed the threshold to downstream nodes
|
|
1687
1753
|
const passedOpportunities = evaluatedOpportunities.filter((o) => o.score >= minScore);
|
|
1688
1754
|
return {
|
|
1755
|
+
candidates: eligibleCandidates,
|
|
1689
1756
|
evaluatedOpportunities: passedOpportunities,
|
|
1690
1757
|
remainingCandidates: effectiveRemaining,
|
|
1691
1758
|
trace: traceEntries,
|
|
@@ -1940,11 +2007,21 @@ export class OpportunityGraphFactory {
|
|
|
1940
2007
|
const counterpartName = candidate.candidateUser.profile?.name ?? '';
|
|
1941
2008
|
const viewerName = sourceUser.profile.name;
|
|
1942
2009
|
const rawReasoning = updated.interpretation?.reasoning ?? '';
|
|
1943
|
-
const personalizedSummary =
|
|
2010
|
+
const personalizedSummary = safeFallbackSummary(rawReasoning, {
|
|
2011
|
+
counterpartName,
|
|
2012
|
+
viewerName,
|
|
2013
|
+
emptyText: 'A suggested connection.',
|
|
2014
|
+
});
|
|
1944
2015
|
traceEmitter?.({
|
|
1945
2016
|
type: 'opportunity_draft_ready',
|
|
1946
2017
|
opportunityId: candidate.opportunityId,
|
|
1947
|
-
opportunity:
|
|
2018
|
+
opportunity: {
|
|
2019
|
+
...updated,
|
|
2020
|
+
interpretation: {
|
|
2021
|
+
...updated.interpretation,
|
|
2022
|
+
reasoning: personalizedSummary,
|
|
2023
|
+
},
|
|
2024
|
+
},
|
|
1948
2025
|
personalizedSummary,
|
|
1949
2026
|
counterparty: {
|
|
1950
2027
|
userId: candidate.candidateUser.id,
|
|
@@ -2347,6 +2424,77 @@ export class OpportunityGraphFactory {
|
|
|
2347
2424
|
return { opportunities: [] };
|
|
2348
2425
|
}
|
|
2349
2426
|
try {
|
|
2427
|
+
// Recompute the authoritative owner-side scope at the final boundary.
|
|
2428
|
+
// The adapter receives this immutable request scope and locks current
|
|
2429
|
+
// memberships plus trigger-intent assignments through commit.
|
|
2430
|
+
const currentOwnerMemberships = await this.database.getNetworkMemberships(state.userId);
|
|
2431
|
+
let finalAllowedNetworkIds = currentOwnerMemberships.map((membership) => membership.networkId);
|
|
2432
|
+
// Only an explicit trigger intent is an authoritative network boundary.
|
|
2433
|
+
// Ad-hoc global discovery may heuristically resolve a matching intent
|
|
2434
|
+
// for ranking, but must retain its all-membership reach.
|
|
2435
|
+
const finalTriggerIntentId = state.triggerIntentId;
|
|
2436
|
+
if (finalTriggerIntentId) {
|
|
2437
|
+
const currentAssignments = new Set(await this.database.getNetworkIdsForIntent(finalTriggerIntentId));
|
|
2438
|
+
finalAllowedNetworkIds = finalAllowedNetworkIds.filter((networkId) => currentAssignments.has(networkId));
|
|
2439
|
+
}
|
|
2440
|
+
const explicitScope = state.networkId
|
|
2441
|
+
? [state.networkId]
|
|
2442
|
+
: state.indexScope;
|
|
2443
|
+
if (explicitScope !== undefined) {
|
|
2444
|
+
const explicitlyAllowed = new Set(explicitScope);
|
|
2445
|
+
finalAllowedNetworkIds = finalAllowedNetworkIds.filter((networkId) => explicitlyAllowed.has(networkId));
|
|
2446
|
+
}
|
|
2447
|
+
finalAllowedNetworkIds = [...new Set(finalAllowedNetworkIds)];
|
|
2448
|
+
if (finalAllowedNetworkIds.length === 0) {
|
|
2449
|
+
persistLog.info('Skipped persistence because final discovery scope is empty', {
|
|
2450
|
+
userId: state.userId,
|
|
2451
|
+
triggerIntentId: finalTriggerIntentId,
|
|
2452
|
+
});
|
|
2453
|
+
return { opportunities: [] };
|
|
2454
|
+
}
|
|
2455
|
+
const finalAllowedNetworks = new Set(finalAllowedNetworkIds);
|
|
2456
|
+
const networkEligibility = {
|
|
2457
|
+
ownerUserId: state.userId,
|
|
2458
|
+
allowedNetworkIds: finalAllowedNetworkIds,
|
|
2459
|
+
...(finalTriggerIntentId ? { triggerIntentId: finalTriggerIntentId } : {}),
|
|
2460
|
+
};
|
|
2461
|
+
// Recheck evaluator participants before any dedup/reactivation/write.
|
|
2462
|
+
// Persistence-only introducers are deliberately absent here: personal-
|
|
2463
|
+
// network contact discovery validates the evaluated owner/candidate
|
|
2464
|
+
// pairs without requiring the owner actor that is added below.
|
|
2465
|
+
const requestedPairs = state.evaluatedOpportunities.flatMap((evaluated) => evaluated.actors.flatMap((actor) => actor.networkId
|
|
2466
|
+
? [{ userId: actor.userId, networkId: actor.networkId }]
|
|
2467
|
+
: []));
|
|
2468
|
+
const activePairs = await this.database.getActiveNetworkMembershipPairs(requestedPairs);
|
|
2469
|
+
const activePairKeys = new Set(activePairs.map((pair) => networkMembershipPairKey(pair.userId, pair.networkId)));
|
|
2470
|
+
const evaluatedToPersist = state.evaluatedOpportunities.filter((evaluated) => evaluated.actors.length > 0
|
|
2471
|
+
&& evaluated.actors.every((actor) => actor.networkId != null
|
|
2472
|
+
&& finalAllowedNetworks.has(actor.networkId)
|
|
2473
|
+
&& activePairKeys.has(networkMembershipPairKey(actor.userId, actor.networkId))));
|
|
2474
|
+
if (evaluatedToPersist.length < state.evaluatedOpportunities.length) {
|
|
2475
|
+
persistLog.info('Skipped opportunities with inactive participant network pairs', {
|
|
2476
|
+
before: state.evaluatedOpportunities.length,
|
|
2477
|
+
after: evaluatedToPersist.length,
|
|
2478
|
+
removed: state.evaluatedOpportunities.length - evaluatedToPersist.length,
|
|
2479
|
+
});
|
|
2480
|
+
}
|
|
2481
|
+
if (evaluatedToPersist.length === 0)
|
|
2482
|
+
return { opportunities: [] };
|
|
2483
|
+
const updateStatusIfStillEligible = async (opportunityId, status, existingActors) => {
|
|
2484
|
+
// Reactivation preserves the existing opportunity row, so lock the
|
|
2485
|
+
// existing participant anchors rather than the evaluator's current
|
|
2486
|
+
// (often network-less) actor output. Introducers do not participate
|
|
2487
|
+
// in matching eligibility and must not suppress a valid pair.
|
|
2488
|
+
const anchors = existingActors.filter((actor) => actor.role !== 'introducer');
|
|
2489
|
+
if (anchors.length === 0 || anchors.some((actor) => !finalAllowedNetworks.has(actor.networkId))) {
|
|
2490
|
+
return null;
|
|
2491
|
+
}
|
|
2492
|
+
if (!this.database.updateOpportunityStatusIfNetworkEligible) {
|
|
2493
|
+
persistLog.error('Network-eligible status update adapter is unavailable; failing closed');
|
|
2494
|
+
return null;
|
|
2495
|
+
}
|
|
2496
|
+
return this.database.updateOpportunityStatusIfNetworkEligible(opportunityId, status, anchors, networkEligibility);
|
|
2497
|
+
};
|
|
2350
2498
|
const itemsToPersist = [];
|
|
2351
2499
|
const reactivatedOpportunities = [];
|
|
2352
2500
|
const existingBetweenActors = [];
|
|
@@ -2374,7 +2522,7 @@ export class OpportunityGraphFactory {
|
|
|
2374
2522
|
// counterparty, not between the introducer and the counterparty.
|
|
2375
2523
|
const dedupUserId = (state.onBehalfOfUserId ?? state.userId);
|
|
2376
2524
|
const uniqueCounterparts = new Set();
|
|
2377
|
-
for (const evaluated of
|
|
2525
|
+
for (const evaluated of evaluatedToPersist) {
|
|
2378
2526
|
const candidateUserId = evaluated.actors.find(a => a.userId !== dedupUserId)?.userId;
|
|
2379
2527
|
if (candidateUserId)
|
|
2380
2528
|
uniqueCounterparts.add(candidateUserId);
|
|
@@ -2394,7 +2542,7 @@ export class OpportunityGraphFactory {
|
|
|
2394
2542
|
}));
|
|
2395
2543
|
dedupAlreadyAccepted.push(...lookups.flat());
|
|
2396
2544
|
}
|
|
2397
|
-
for (const evaluated of
|
|
2545
|
+
for (const evaluated of evaluatedToPersist) {
|
|
2398
2546
|
const indexIdForActors = state.networkId ?? evaluated.actors[0]?.networkId;
|
|
2399
2547
|
let actors;
|
|
2400
2548
|
let data;
|
|
@@ -2493,7 +2641,7 @@ export class OpportunityGraphFactory {
|
|
|
2493
2641
|
if (existing.status === 'stalled' || sameIntroducer) {
|
|
2494
2642
|
// Introduction path always targets 'draft' (chat-only surface) rather than using
|
|
2495
2643
|
// initialStatus, because introductions are always chat-initiated, not background-discovered.
|
|
2496
|
-
const reactivated = await
|
|
2644
|
+
const reactivated = await updateStatusIfStillEligible(existing.id, 'draft', existing.actors);
|
|
2497
2645
|
if (reactivated) {
|
|
2498
2646
|
persistLog.verbose('Reactivated opportunity (introduction path)', {
|
|
2499
2647
|
opportunityId: existing.id,
|
|
@@ -2526,7 +2674,7 @@ export class OpportunityGraphFactory {
|
|
|
2526
2674
|
continue;
|
|
2527
2675
|
}
|
|
2528
2676
|
}
|
|
2529
|
-
const reactivated = await
|
|
2677
|
+
const reactivated = await updateStatusIfStillEligible(existing.id, 'draft', existing.actors);
|
|
2530
2678
|
if (reactivated) {
|
|
2531
2679
|
persistLog.info('Resuming orphaned negotiating opportunity (introduction path)', {
|
|
2532
2680
|
opportunityId: existing.id,
|
|
@@ -2539,7 +2687,7 @@ export class OpportunityGraphFactory {
|
|
|
2539
2687
|
}
|
|
2540
2688
|
else if (existing.status === 'latent') {
|
|
2541
2689
|
// Upgrade latent to draft for introduction path
|
|
2542
|
-
const upgraded = await
|
|
2690
|
+
const upgraded = await updateStatusIfStillEligible(existing.id, 'draft', existing.actors);
|
|
2543
2691
|
if (upgraded) {
|
|
2544
2692
|
persistLog.verbose('Upgraded latent opportunity to draft (introduction path)', {
|
|
2545
2693
|
opportunityId: existing.id,
|
|
@@ -2655,7 +2803,7 @@ export class OpportunityGraphFactory {
|
|
|
2655
2803
|
// Reactivate expired or stalled opportunities.
|
|
2656
2804
|
// Stalled opportunities are reactivated regardless of age: a stalled negotiation
|
|
2657
2805
|
// is still in-flight for this pair, so we resume it rather than create a parallel one.
|
|
2658
|
-
const reactivated = await
|
|
2806
|
+
const reactivated = await updateStatusIfStillEligible(existing.id, initialStatus, existing.actors);
|
|
2659
2807
|
if (reactivated) {
|
|
2660
2808
|
persistLog.verbose('Reactivated opportunity', {
|
|
2661
2809
|
opportunityId: existing.id,
|
|
@@ -2691,7 +2839,7 @@ export class OpportunityGraphFactory {
|
|
|
2691
2839
|
}
|
|
2692
2840
|
}
|
|
2693
2841
|
// Task is stale or missing — reactivate the orphaned negotiating opportunity
|
|
2694
|
-
const reactivated = await
|
|
2842
|
+
const reactivated = await updateStatusIfStillEligible(existing.id, initialStatus, existing.actors);
|
|
2695
2843
|
if (reactivated) {
|
|
2696
2844
|
persistLog.info('Resuming orphaned negotiating opportunity', {
|
|
2697
2845
|
opportunityId: existing.id,
|
|
@@ -2704,7 +2852,7 @@ export class OpportunityGraphFactory {
|
|
|
2704
2852
|
}
|
|
2705
2853
|
else if (existing.status === 'latent' && initialStatus !== 'latent') {
|
|
2706
2854
|
// Upgrade latent (background-discovered) to the higher-priority status (e.g. pending)
|
|
2707
|
-
const upgraded = await
|
|
2855
|
+
const upgraded = await updateStatusIfStillEligible(existing.id, initialStatus, existing.actors);
|
|
2708
2856
|
if (upgraded) {
|
|
2709
2857
|
persistLog.verbose('Upgraded latent opportunity to higher-priority status', {
|
|
2710
2858
|
opportunityId: existing.id,
|
|
@@ -2774,6 +2922,13 @@ export class OpportunityGraphFactory {
|
|
|
2774
2922
|
},
|
|
2775
2923
|
};
|
|
2776
2924
|
}
|
|
2925
|
+
if (hasUnsupportedOpportunityClaim(data.interpretation.reasoning)) {
|
|
2926
|
+
persistLog.warn('Skipping opportunity with unsupported affiliation/presence claim at persistence boundary', {
|
|
2927
|
+
source: data.detection.source,
|
|
2928
|
+
triggerIntentId: data.detection.triggeredBy,
|
|
2929
|
+
});
|
|
2930
|
+
continue;
|
|
2931
|
+
}
|
|
2777
2932
|
try {
|
|
2778
2933
|
validateOpportunityActors(data.actors);
|
|
2779
2934
|
}
|
|
@@ -2841,6 +2996,7 @@ export class OpportunityGraphFactory {
|
|
|
2841
2996
|
database: this.database,
|
|
2842
2997
|
embedder: this.embedder,
|
|
2843
2998
|
items: itemsForPersistence,
|
|
2999
|
+
networkEligibility,
|
|
2844
3000
|
});
|
|
2845
3001
|
const allOpportunities = [...reactivatedOpportunities, ...createdList];
|
|
2846
3002
|
persistLog.verbose('Persistence complete', {
|
|
@@ -2990,7 +3146,10 @@ export class OpportunityGraphFactory {
|
|
|
2990
3146
|
indexName: indexRecord?.title ?? (actorIndexId ?? ''),
|
|
2991
3147
|
connectedWith,
|
|
2992
3148
|
suggestedBy,
|
|
2993
|
-
reasoning: opp.interpretation?.reasoning
|
|
3149
|
+
reasoning: safeFallbackSummary(opp.interpretation?.reasoning, {
|
|
3150
|
+
counterpartName: connectedWith.join(' and '),
|
|
3151
|
+
emptyText: 'Connection opportunity',
|
|
3152
|
+
}),
|
|
2994
3153
|
status: opp.status,
|
|
2995
3154
|
category,
|
|
2996
3155
|
confidence: confidence != null ? confidence : null,
|