@indexnetwork/protocol 4.16.0-rc.358.1 → 4.17.0-rc.360.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 +8 -1
- 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 +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -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 +23 -0
- package/dist/opportunity/opportunity.graph.d.ts.map +1 -1
- package/dist/opportunity/opportunity.graph.js +172 -17
- 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.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/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 +1 -1
|
@@ -20,7 +20,8 @@ import { getModelName } from '../shared/agent/model.config.js';
|
|
|
20
20
|
import { selectHydeDocumentsForGeneration } from '../shared/hyde/hyde.documents.js';
|
|
21
21
|
import { getHydeGenerationMode } from '../shared/hyde/hyde.env.js';
|
|
22
22
|
import { validateOpportunityActors } from './opportunity.utils.js';
|
|
23
|
-
import {
|
|
23
|
+
import { safeFallbackSummary } from './opportunity.safe-presentation.js';
|
|
24
|
+
import { hasUnsupportedOpportunityClaim } from './opportunity.claim-safety.js';
|
|
24
25
|
import { persistOpportunities } from './opportunity.persist.js';
|
|
25
26
|
import { INTRODUCER_DISCOVERY_SOURCE } from './opportunity.introducer.js';
|
|
26
27
|
import { negotiateCandidates } from "../negotiation/negotiation.graph.js";
|
|
@@ -54,6 +55,9 @@ const routingLog = protocolLogger('OpportunityGraph:Routing');
|
|
|
54
55
|
const DEDUP_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
|
|
55
56
|
/** Default cap for source premises used by premise-to-premise discovery. Prevents BACKEND-5-style fan-out. */
|
|
56
57
|
const DEFAULT_SOURCE_PREMISE_DISCOVERY_LIMIT = 40;
|
|
58
|
+
function networkMembershipPairKey(userId, networkId) {
|
|
59
|
+
return `${userId}\u0000${networkId}`;
|
|
60
|
+
}
|
|
57
61
|
/** Per-source cap for candidate premise matches. */
|
|
58
62
|
const PREMISE_MATCH_LIMIT_PER_SOURCE = 20;
|
|
59
63
|
/** Resolve the source premise discovery cap from env, preserving 0 as an explicit disable switch. */
|
|
@@ -337,10 +341,11 @@ export class OpportunityGraphFactory {
|
|
|
337
341
|
}
|
|
338
342
|
targetIndexIds = [state.networkId];
|
|
339
343
|
}
|
|
340
|
-
else if (state.indexScope
|
|
344
|
+
else if (state.indexScope !== undefined) {
|
|
341
345
|
// Bounded scope (e.g. a network-scoped agent's reachable networks):
|
|
342
346
|
// intersect with the user's actual memberships so discovery never
|
|
343
|
-
// 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.
|
|
344
349
|
const allowed = new Set(state.indexScope);
|
|
345
350
|
targetIndexIds = state.userNetworks.filter((n) => allowed.has(n));
|
|
346
351
|
scopeLog.verbose('Applied indexScope intersection', {
|
|
@@ -353,6 +358,19 @@ export class OpportunityGraphFactory {
|
|
|
353
358
|
// Search all user's indexes
|
|
354
359
|
targetIndexIds = state.userNetworks;
|
|
355
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
|
+
}
|
|
356
374
|
// Fetch index details
|
|
357
375
|
const targetNetworks = await Promise.all(targetIndexIds.map(async (networkId) => {
|
|
358
376
|
const index = await this.database.getNetwork(networkId);
|
|
@@ -486,6 +504,19 @@ export class OpportunityGraphFactory {
|
|
|
486
504
|
try {
|
|
487
505
|
let resolvedIntentId;
|
|
488
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
|
+
}
|
|
489
520
|
const inNetwork = await this.database.getNetworkIdsForIntent(state.triggerIntentId);
|
|
490
521
|
const inTarget = inNetwork.some((id) => targetIndexIds.includes(id));
|
|
491
522
|
resolvedIntentId = state.triggerIntentId;
|
|
@@ -1323,6 +1354,38 @@ export class OpportunityGraphFactory {
|
|
|
1323
1354
|
const dedupedCandidates = Array.from(bestByUser.values());
|
|
1324
1355
|
// Re-sort by similarity descending (Map iteration order doesn't guarantee sort)
|
|
1325
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
|
+
}
|
|
1326
1389
|
if (dedupedCandidates.length < sortedCandidates.length) {
|
|
1327
1390
|
evaluationLog.info("Deduped candidates by userId", {
|
|
1328
1391
|
before: sortedCandidates.length,
|
|
@@ -1330,8 +1393,8 @@ export class OpportunityGraphFactory {
|
|
|
1330
1393
|
removed: sortedCandidates.length - dedupedCandidates.length,
|
|
1331
1394
|
});
|
|
1332
1395
|
}
|
|
1333
|
-
const batchToEvaluate =
|
|
1334
|
-
const remaining =
|
|
1396
|
+
const batchToEvaluate = eligibleCandidates.slice(0, EVAL_BATCH_SIZE);
|
|
1397
|
+
const remaining = eligibleCandidates.slice(EVAL_BATCH_SIZE);
|
|
1335
1398
|
// Early termination: if search was query-driven and no query-sourced candidates remain,
|
|
1336
1399
|
// clear remaining to prevent pointless pagination through non-query leftovers
|
|
1337
1400
|
const isQueryDriven = !!state.searchQuery?.trim();
|
|
@@ -1351,7 +1414,6 @@ export class OpportunityGraphFactory {
|
|
|
1351
1414
|
}
|
|
1352
1415
|
const agentTimingsAccum = [];
|
|
1353
1416
|
try {
|
|
1354
|
-
const discoveryUserId = state.onBehalfOfUserId ?? state.userId;
|
|
1355
1417
|
const sourceProfile = await this.database.getProfile(discoveryUserId);
|
|
1356
1418
|
const sourceEntity = {
|
|
1357
1419
|
userId: discoveryUserId,
|
|
@@ -1690,6 +1752,7 @@ export class OpportunityGraphFactory {
|
|
|
1690
1752
|
// Only pass opportunities that passed the threshold to downstream nodes
|
|
1691
1753
|
const passedOpportunities = evaluatedOpportunities.filter((o) => o.score >= minScore);
|
|
1692
1754
|
return {
|
|
1755
|
+
candidates: eligibleCandidates,
|
|
1693
1756
|
evaluatedOpportunities: passedOpportunities,
|
|
1694
1757
|
remainingCandidates: effectiveRemaining,
|
|
1695
1758
|
trace: traceEntries,
|
|
@@ -1944,11 +2007,21 @@ export class OpportunityGraphFactory {
|
|
|
1944
2007
|
const counterpartName = candidate.candidateUser.profile?.name ?? '';
|
|
1945
2008
|
const viewerName = sourceUser.profile.name;
|
|
1946
2009
|
const rawReasoning = updated.interpretation?.reasoning ?? '';
|
|
1947
|
-
const personalizedSummary =
|
|
2010
|
+
const personalizedSummary = safeFallbackSummary(rawReasoning, {
|
|
2011
|
+
counterpartName,
|
|
2012
|
+
viewerName,
|
|
2013
|
+
emptyText: 'A suggested connection.',
|
|
2014
|
+
});
|
|
1948
2015
|
traceEmitter?.({
|
|
1949
2016
|
type: 'opportunity_draft_ready',
|
|
1950
2017
|
opportunityId: candidate.opportunityId,
|
|
1951
|
-
opportunity:
|
|
2018
|
+
opportunity: {
|
|
2019
|
+
...updated,
|
|
2020
|
+
interpretation: {
|
|
2021
|
+
...updated.interpretation,
|
|
2022
|
+
reasoning: personalizedSummary,
|
|
2023
|
+
},
|
|
2024
|
+
},
|
|
1952
2025
|
personalizedSummary,
|
|
1953
2026
|
counterparty: {
|
|
1954
2027
|
userId: candidate.candidateUser.id,
|
|
@@ -2351,6 +2424,77 @@ export class OpportunityGraphFactory {
|
|
|
2351
2424
|
return { opportunities: [] };
|
|
2352
2425
|
}
|
|
2353
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
|
+
};
|
|
2354
2498
|
const itemsToPersist = [];
|
|
2355
2499
|
const reactivatedOpportunities = [];
|
|
2356
2500
|
const existingBetweenActors = [];
|
|
@@ -2378,7 +2522,7 @@ export class OpportunityGraphFactory {
|
|
|
2378
2522
|
// counterparty, not between the introducer and the counterparty.
|
|
2379
2523
|
const dedupUserId = (state.onBehalfOfUserId ?? state.userId);
|
|
2380
2524
|
const uniqueCounterparts = new Set();
|
|
2381
|
-
for (const evaluated of
|
|
2525
|
+
for (const evaluated of evaluatedToPersist) {
|
|
2382
2526
|
const candidateUserId = evaluated.actors.find(a => a.userId !== dedupUserId)?.userId;
|
|
2383
2527
|
if (candidateUserId)
|
|
2384
2528
|
uniqueCounterparts.add(candidateUserId);
|
|
@@ -2398,7 +2542,7 @@ export class OpportunityGraphFactory {
|
|
|
2398
2542
|
}));
|
|
2399
2543
|
dedupAlreadyAccepted.push(...lookups.flat());
|
|
2400
2544
|
}
|
|
2401
|
-
for (const evaluated of
|
|
2545
|
+
for (const evaluated of evaluatedToPersist) {
|
|
2402
2546
|
const indexIdForActors = state.networkId ?? evaluated.actors[0]?.networkId;
|
|
2403
2547
|
let actors;
|
|
2404
2548
|
let data;
|
|
@@ -2497,7 +2641,7 @@ export class OpportunityGraphFactory {
|
|
|
2497
2641
|
if (existing.status === 'stalled' || sameIntroducer) {
|
|
2498
2642
|
// Introduction path always targets 'draft' (chat-only surface) rather than using
|
|
2499
2643
|
// initialStatus, because introductions are always chat-initiated, not background-discovered.
|
|
2500
|
-
const reactivated = await
|
|
2644
|
+
const reactivated = await updateStatusIfStillEligible(existing.id, 'draft', existing.actors);
|
|
2501
2645
|
if (reactivated) {
|
|
2502
2646
|
persistLog.verbose('Reactivated opportunity (introduction path)', {
|
|
2503
2647
|
opportunityId: existing.id,
|
|
@@ -2530,7 +2674,7 @@ export class OpportunityGraphFactory {
|
|
|
2530
2674
|
continue;
|
|
2531
2675
|
}
|
|
2532
2676
|
}
|
|
2533
|
-
const reactivated = await
|
|
2677
|
+
const reactivated = await updateStatusIfStillEligible(existing.id, 'draft', existing.actors);
|
|
2534
2678
|
if (reactivated) {
|
|
2535
2679
|
persistLog.info('Resuming orphaned negotiating opportunity (introduction path)', {
|
|
2536
2680
|
opportunityId: existing.id,
|
|
@@ -2543,7 +2687,7 @@ export class OpportunityGraphFactory {
|
|
|
2543
2687
|
}
|
|
2544
2688
|
else if (existing.status === 'latent') {
|
|
2545
2689
|
// Upgrade latent to draft for introduction path
|
|
2546
|
-
const upgraded = await
|
|
2690
|
+
const upgraded = await updateStatusIfStillEligible(existing.id, 'draft', existing.actors);
|
|
2547
2691
|
if (upgraded) {
|
|
2548
2692
|
persistLog.verbose('Upgraded latent opportunity to draft (introduction path)', {
|
|
2549
2693
|
opportunityId: existing.id,
|
|
@@ -2659,7 +2803,7 @@ export class OpportunityGraphFactory {
|
|
|
2659
2803
|
// Reactivate expired or stalled opportunities.
|
|
2660
2804
|
// Stalled opportunities are reactivated regardless of age: a stalled negotiation
|
|
2661
2805
|
// is still in-flight for this pair, so we resume it rather than create a parallel one.
|
|
2662
|
-
const reactivated = await
|
|
2806
|
+
const reactivated = await updateStatusIfStillEligible(existing.id, initialStatus, existing.actors);
|
|
2663
2807
|
if (reactivated) {
|
|
2664
2808
|
persistLog.verbose('Reactivated opportunity', {
|
|
2665
2809
|
opportunityId: existing.id,
|
|
@@ -2695,7 +2839,7 @@ export class OpportunityGraphFactory {
|
|
|
2695
2839
|
}
|
|
2696
2840
|
}
|
|
2697
2841
|
// Task is stale or missing — reactivate the orphaned negotiating opportunity
|
|
2698
|
-
const reactivated = await
|
|
2842
|
+
const reactivated = await updateStatusIfStillEligible(existing.id, initialStatus, existing.actors);
|
|
2699
2843
|
if (reactivated) {
|
|
2700
2844
|
persistLog.info('Resuming orphaned negotiating opportunity', {
|
|
2701
2845
|
opportunityId: existing.id,
|
|
@@ -2708,7 +2852,7 @@ export class OpportunityGraphFactory {
|
|
|
2708
2852
|
}
|
|
2709
2853
|
else if (existing.status === 'latent' && initialStatus !== 'latent') {
|
|
2710
2854
|
// Upgrade latent (background-discovered) to the higher-priority status (e.g. pending)
|
|
2711
|
-
const upgraded = await
|
|
2855
|
+
const upgraded = await updateStatusIfStillEligible(existing.id, initialStatus, existing.actors);
|
|
2712
2856
|
if (upgraded) {
|
|
2713
2857
|
persistLog.verbose('Upgraded latent opportunity to higher-priority status', {
|
|
2714
2858
|
opportunityId: existing.id,
|
|
@@ -2778,6 +2922,13 @@ export class OpportunityGraphFactory {
|
|
|
2778
2922
|
},
|
|
2779
2923
|
};
|
|
2780
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
|
+
}
|
|
2781
2932
|
try {
|
|
2782
2933
|
validateOpportunityActors(data.actors);
|
|
2783
2934
|
}
|
|
@@ -2845,6 +2996,7 @@ export class OpportunityGraphFactory {
|
|
|
2845
2996
|
database: this.database,
|
|
2846
2997
|
embedder: this.embedder,
|
|
2847
2998
|
items: itemsForPersistence,
|
|
2999
|
+
networkEligibility,
|
|
2848
3000
|
});
|
|
2849
3001
|
const allOpportunities = [...reactivatedOpportunities, ...createdList];
|
|
2850
3002
|
persistLog.verbose('Persistence complete', {
|
|
@@ -2994,7 +3146,10 @@ export class OpportunityGraphFactory {
|
|
|
2994
3146
|
indexName: indexRecord?.title ?? (actorIndexId ?? ''),
|
|
2995
3147
|
connectedWith,
|
|
2996
3148
|
suggestedBy,
|
|
2997
|
-
reasoning: opp.interpretation?.reasoning
|
|
3149
|
+
reasoning: safeFallbackSummary(opp.interpretation?.reasoning, {
|
|
3150
|
+
counterpartName: connectedWith.join(' and '),
|
|
3151
|
+
emptyText: 'Connection opportunity',
|
|
3152
|
+
}),
|
|
2998
3153
|
status: opp.status,
|
|
2999
3154
|
category,
|
|
3000
3155
|
confidence: confidence != null ? confidence : null,
|