@indexnetwork/protocol 14.3.2-rc.477.1 → 16.1.1-rc.478.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 +107 -0
- package/IMPLEMENTATION.md +50 -52
- package/STABILITY.md +3 -3
- package/dist/chat/chat.agent.js +2 -2
- package/dist/chat/chat.graph.d.ts +41 -1
- package/dist/chat/chat.graph.js +108 -127
- package/dist/discovery/hyde.frame.d.ts +4 -4
- package/dist/discovery/hyde.graph.d.ts +71 -12
- package/dist/discovery/hyde.graph.js +337 -335
- package/dist/discovery/lens.inferrer.d.ts +6 -6
- package/dist/enrichment/enrichment.graph.d.ts +127 -9
- package/dist/enrichment/enrichment.graph.js +633 -655
- package/dist/enrichment/enrichment.state.d.ts +1 -1
- package/dist/enrichment/enrichment.tools.context-read.d.ts +10 -0
- package/dist/enrichment/enrichment.tools.context-read.js +353 -0
- package/dist/enrichment/enrichment.tools.context-write.d.ts +9 -0
- package/dist/enrichment/enrichment.tools.context-write.js +417 -0
- package/dist/enrichment/enrichment.tools.d.ts +11 -1
- package/dist/enrichment/enrichment.tools.helpers.d.ts +139 -0
- package/dist/enrichment/enrichment.tools.helpers.js +234 -0
- package/dist/enrichment/enrichment.tools.js +16 -975
- package/dist/index.d.ts +27 -48
- package/dist/index.js +17 -27
- package/dist/intents/application/intent.graph.d.ts +71 -67
- package/dist/intents/application/intent.graph.execute.d.ts +59 -0
- package/dist/intents/application/intent.graph.execute.js +301 -0
- package/dist/intents/application/intent.graph.infer.d.ts +44 -0
- package/dist/intents/application/intent.graph.infer.js +97 -0
- package/dist/intents/application/intent.graph.js +96 -888
- package/dist/intents/application/intent.graph.reconcile.d.ts +71 -0
- package/dist/intents/application/intent.graph.reconcile.js +274 -0
- package/dist/intents/application/intent.graph.shared.d.ts +70 -0
- package/dist/intents/application/intent.graph.shared.js +153 -0
- package/dist/intents/domain/intent.state.d.ts +1 -1
- package/dist/maintenance/maintenance.graph.d.ts +66 -3
- package/dist/maintenance/maintenance.graph.js +155 -156
- package/dist/mcp/mcp.authorization-policy.d.ts +24 -28
- package/dist/mcp/mcp.authorization-policy.js +14 -34
- package/dist/mcp/mcp.server.d.ts +3 -3
- package/dist/mcp/mcp.server.js +12 -16
- package/dist/negotiations/application/negotiation.candidates.d.ts +83 -0
- package/dist/negotiations/application/negotiation.candidates.js +162 -0
- package/dist/negotiations/application/negotiation.graph.d.ts +91 -157
- package/dist/negotiations/application/negotiation.graph.finalize.d.ts +5 -0
- package/dist/negotiations/application/negotiation.graph.finalize.js +280 -0
- package/dist/negotiations/application/negotiation.graph.init.d.ts +54 -0
- package/dist/negotiations/application/negotiation.graph.init.js +227 -0
- package/dist/negotiations/application/negotiation.graph.js +69 -1388
- package/dist/negotiations/application/negotiation.graph.screen.d.ts +23 -0
- package/dist/negotiations/application/negotiation.graph.screen.js +108 -0
- package/dist/negotiations/application/negotiation.graph.shared.d.ts +75 -0
- package/dist/negotiations/application/negotiation.graph.shared.js +125 -0
- package/dist/negotiations/application/negotiation.graph.turn.d.ts +105 -0
- package/dist/negotiations/application/negotiation.graph.turn.js +484 -0
- package/dist/negotiations/domain/negotiation.state.d.ts +1 -1
- package/dist/negotiations/domain/negotiation.state.js +0 -1
- package/dist/networks/application/indexer.graph.d.ts +165 -7
- package/dist/networks/application/indexer.graph.js +339 -388
- package/dist/networks/application/indexer.state.d.ts +1 -1
- package/dist/networks/application/membership.graph.d.ts +83 -5
- package/dist/networks/application/membership.graph.js +177 -207
- package/dist/networks/application/network.graph.d.ts +148 -5
- package/dist/networks/application/network.graph.js +249 -278
- package/dist/networks/domain/membership.state.d.ts +1 -1
- package/dist/networks/domain/network.state.d.ts +1 -1
- package/dist/opportunities/application/delivery-card.cache.d.ts +1 -1
- package/dist/opportunities/application/delivery-card.cache.js +2 -2
- package/dist/opportunities/application/index.d.ts +2 -2
- package/dist/opportunities/application/index.js +2 -2
- package/dist/opportunities/application/opportunity.evaluator.js +1 -1
- package/dist/opportunities/application/opportunity.graph.d.ts +686 -473
- package/dist/opportunities/application/opportunity.graph.discovery-strategies.d.ts +109 -0
- package/dist/opportunities/application/opportunity.graph.discovery-strategies.js +451 -0
- package/dist/opportunities/application/opportunity.graph.discovery.d.ts +69 -0
- package/dist/opportunities/application/opportunity.graph.discovery.js +397 -0
- package/dist/opportunities/application/opportunity.graph.evaluation.d.ts +82 -0
- package/dist/opportunities/application/opportunity.graph.evaluation.js +608 -0
- package/dist/opportunities/application/opportunity.graph.js +120 -3690
- package/dist/opportunities/application/opportunity.graph.modes.d.ts +538 -0
- package/dist/opportunities/application/opportunity.graph.modes.js +538 -0
- package/dist/opportunities/application/opportunity.graph.negotiate.d.ts +109 -0
- package/dist/opportunities/application/opportunity.graph.negotiate.js +392 -0
- package/dist/opportunities/application/opportunity.graph.persist-node.d.ts +93 -0
- package/dist/opportunities/application/opportunity.graph.persist-node.js +765 -0
- package/dist/opportunities/application/opportunity.graph.prep.d.ts +150 -0
- package/dist/opportunities/application/opportunity.graph.prep.js +381 -0
- package/dist/opportunities/application/opportunity.graph.shared.d.ts +173 -0
- package/dist/opportunities/application/opportunity.graph.shared.js +199 -0
- package/dist/opportunities/application/opportunity.presentation.d.ts +399 -0
- package/dist/opportunities/application/{opportunity.presenter.js → opportunity.presentation.js} +735 -9
- package/dist/opportunities/application/opportunity.tools.cards.d.ts +152 -0
- package/dist/opportunities/application/opportunity.tools.cards.js +235 -0
- package/dist/opportunities/application/opportunity.tools.d.ts +8 -114
- package/dist/opportunities/application/opportunity.tools.js +28 -711
- package/dist/opportunities/application/opportunity.tools.list.d.ts +10 -0
- package/dist/opportunities/application/opportunity.tools.list.js +493 -0
- package/dist/opportunities/domain/index.d.ts +3 -3
- package/dist/opportunities/domain/index.js +3 -3
- package/dist/opportunities/domain/opportunity.state.d.ts +19 -19
- package/dist/opportunities/index.d.ts +6 -6
- package/dist/opportunities/index.js +4 -4
- package/dist/opportunities/ports/opportunity.tools.port.d.ts +1 -1
- package/dist/opportunities/radar/radar.graph.d.ts +57 -13
- package/dist/opportunities/radar/radar.graph.js +470 -471
- package/dist/premises/premise.graph.d.ts +88 -20
- package/dist/premises/premise.graph.js +207 -218
- package/dist/questions/domain/question.schema.d.ts +40 -40
- package/dist/shared/agent/tool.helpers.d.ts +21 -0
- package/dist/shared/agent/tool.registry.d.ts +4 -3
- package/dist/shared/agent/tool.registry.js +1 -1
- package/dist/shared/interfaces/database.capabilities.d.ts +100 -0
- package/dist/shared/interfaces/database.capabilities.js +7 -0
- package/dist/shared/interfaces/database.entities.d.ts +533 -0
- package/dist/shared/interfaces/database.entities.js +10 -0
- package/dist/shared/interfaces/database.identity-queries.d.ts +294 -0
- package/dist/shared/interfaces/database.identity-queries.js +4 -0
- package/dist/shared/interfaces/database.interface.d.ts +15 -2277
- package/dist/shared/interfaces/database.interface.js +8 -0
- package/dist/shared/interfaces/database.member-queries.d.ts +228 -0
- package/dist/shared/interfaces/database.member-queries.js +4 -0
- package/dist/shared/interfaces/database.negotiation.d.ts +305 -0
- package/dist/shared/interfaces/database.negotiation.js +26 -0
- package/dist/shared/interfaces/database.network-queries.d.ts +277 -0
- package/dist/shared/interfaces/database.network-queries.js +4 -0
- package/dist/shared/interfaces/database.opportunity-queries.d.ts +285 -0
- package/dist/shared/interfaces/database.opportunity-queries.js +4 -0
- package/dist/shared/interfaces/database.port.d.ts +322 -0
- package/dist/shared/interfaces/database.port.js +29 -0
- package/dist/shared/schemas/discovery-question.schema.d.ts +14 -14
- package/dist/shared/schemas/negotiation-digest.schema.d.ts +2 -2
- package/package.json +2 -15
- package/dist/opportunities/application/opportunity.card-presentation.d.ts +0 -44
- package/dist/opportunities/application/opportunity.card-presentation.js +0 -93
- package/dist/opportunities/application/opportunity.presenter.d.ts +0 -156
- package/dist/opportunities/domain/opportunity.presentation-cache.d.ts +0 -5
- package/dist/opportunities/domain/opportunity.presentation-cache.js +0 -11
- package/dist/opportunities/domain/opportunity.presentation.d.ts +0 -76
- package/dist/opportunities/domain/opportunity.presentation.js +0 -516
- package/dist/opportunities/domain/opportunity.safe-presentation.d.ts +0 -104
- package/dist/opportunities/domain/opportunity.safe-presentation.js +0 -102
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discovery pipeline, stage 7: post-persist negotiation.
|
|
3
|
+
*
|
|
4
|
+
* Runs bilateral negotiation per persisted opportunity, passing opportunityId so the
|
|
5
|
+
* negotiation graph's finalize node updates each opportunity's status:
|
|
6
|
+
* accept → 'pending' (sender notification follows the pending → notification path)
|
|
7
|
+
* reject → 'rejected'
|
|
8
|
+
* timeout/turn_cap → 'stalled'
|
|
9
|
+
* Status updates land in the DB; in-memory state.opportunities is not mutated.
|
|
10
|
+
*/
|
|
11
|
+
import { requestContext } from '../../shared/observability/request-context.js';
|
|
12
|
+
import { AMBIENT_PARK_WINDOW_MS, negotiateCandidates } from '../../negotiations/index.js';
|
|
13
|
+
import { buildDiscoverySummary, toDiscoveryNegotiation } from '../domain/negotiation-summary.builder.js';
|
|
14
|
+
import { resolveOpportunityActorIntent } from '../domain/opportunity.actor.js';
|
|
15
|
+
import { buildPrioritizedNegotiationIntents, negotiationIncludesOtherIntents } from './opportunity.existing-negotiation.js';
|
|
16
|
+
import { logger, negotiateLog } from './opportunity.graph.shared.js';
|
|
17
|
+
/** Distinguishes "the budget timer won the race" from a real negotiation result. */
|
|
18
|
+
const NEGOTIATE_TIMER_SENTINEL = Symbol('negotiate-timer-sentinel');
|
|
19
|
+
/**
|
|
20
|
+
* Node 3b: Negotiate (post-persist)
|
|
21
|
+
*/
|
|
22
|
+
export async function negotiateNode(state, deps) {
|
|
23
|
+
if (!deps.negotiationGraph)
|
|
24
|
+
return {};
|
|
25
|
+
if (!state.opportunities || state.opportunities.length === 0)
|
|
26
|
+
return {};
|
|
27
|
+
const traceEmitter = requestContext.getStore()?.traceEmitter;
|
|
28
|
+
const graphStart = Date.now();
|
|
29
|
+
const persistedById = new Map(state.opportunities.map((opportunity) => [opportunity.id, opportunity]));
|
|
30
|
+
const attemptBoundaryById = new Map(state.opportunities.map((opportunity) => [opportunity.id, opportunity.updatedAt]));
|
|
31
|
+
const compensateTasklessNegotiatingOpportunity = async (opportunityId) => {
|
|
32
|
+
const opportunity = persistedById.get(opportunityId);
|
|
33
|
+
const expectedUpdatedAt = attemptBoundaryById.get(opportunityId);
|
|
34
|
+
if (opportunity?.status !== 'negotiating' || !expectedUpdatedAt)
|
|
35
|
+
return;
|
|
36
|
+
const fallbackStatus = opportunity.actors.some((actor) => actor.role === 'introducer')
|
|
37
|
+
? 'latent'
|
|
38
|
+
: 'draft';
|
|
39
|
+
await deps.database
|
|
40
|
+
.compensateTasklessNegotiatingOpportunity(opportunityId, expectedUpdatedAt, fallbackStatus)
|
|
41
|
+
.catch((error) => {
|
|
42
|
+
negotiateLog.warn('Failed to compensate taskless negotiating opportunity', {
|
|
43
|
+
opportunityId,
|
|
44
|
+
expectedUpdatedAt,
|
|
45
|
+
fallbackStatus,
|
|
46
|
+
error,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
traceEmitter?.({ type: "graph_start", name: "Negotiation graph" });
|
|
51
|
+
try {
|
|
52
|
+
// Use the same discoveryUserId pattern as evaluationNode
|
|
53
|
+
const discoveryUserId = (state.onBehalfOfUserId ?? state.userId);
|
|
54
|
+
const includeOtherIntents = negotiationIncludesOtherIntents();
|
|
55
|
+
const sourceAccount = await deps.database.getUser(discoveryUserId).catch(() => null);
|
|
56
|
+
const sourceIntentInputs = (state.indexedIntents ?? []).map((intent) => ({
|
|
57
|
+
id: intent.intentId,
|
|
58
|
+
summary: intent.summary ?? null,
|
|
59
|
+
payload: intent.payload ?? null,
|
|
60
|
+
}));
|
|
61
|
+
const sourceHasExactIntent = sourceIntentInputs.some((intent) => intent.id === state.triggerIntentId);
|
|
62
|
+
const sourceFallbackIntent = state.triggerIntentId && !sourceHasExactIntent
|
|
63
|
+
? await deps.database.getIntent(state.triggerIntentId).catch(() => null)
|
|
64
|
+
: null;
|
|
65
|
+
const ownedSourceFallback = sourceFallbackIntent?.userId === discoveryUserId
|
|
66
|
+
? sourceFallbackIntent
|
|
67
|
+
: null;
|
|
68
|
+
const sourceUser = {
|
|
69
|
+
id: discoveryUserId,
|
|
70
|
+
intents: buildPrioritizedNegotiationIntents(sourceIntentInputs, state.triggerIntentId, ownedSourceFallback, includeOtherIntents),
|
|
71
|
+
profile: {
|
|
72
|
+
name: state.sourceProfile?.identity?.name ?? sourceAccount?.name,
|
|
73
|
+
bio: state.sourceProfile?.identity?.bio ?? sourceAccount?.intro ?? undefined,
|
|
74
|
+
location: state.sourceProfile?.identity?.location ?? sourceAccount?.location ?? undefined,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
// Build candidates from persisted opportunities. Each opportunity carries its DB id
|
|
78
|
+
// so the negotiation graph's finalize node can update its status from the outcome.
|
|
79
|
+
negotiateLog.verbose('Building candidates from opportunities', {
|
|
80
|
+
opportunityCount: state.opportunities.length,
|
|
81
|
+
discoveryUserId,
|
|
82
|
+
});
|
|
83
|
+
const filteredBeforeInvocation = [];
|
|
84
|
+
const candidateEntries = state.opportunities
|
|
85
|
+
.map(opp => {
|
|
86
|
+
// Skip opportunities where any introducer exists but has not yet approved.
|
|
87
|
+
const introducerActors = opp.actors
|
|
88
|
+
.filter(a => a.role === 'introducer');
|
|
89
|
+
if (introducerActors.length > 0 && !introducerActors.every(a => a.approved === true)) {
|
|
90
|
+
negotiateLog.verbose('Skipping opportunity: introducer not approved', {
|
|
91
|
+
opportunityId: opp.id,
|
|
92
|
+
introducerCount: introducerActors.length,
|
|
93
|
+
approvedCount: introducerActors.filter(a => a.approved === true).length,
|
|
94
|
+
});
|
|
95
|
+
filteredBeforeInvocation.push(opp.id);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const opportunityActors = opp.actors;
|
|
99
|
+
const sourceActor = opportunityActors.find(a => a.userId === discoveryUserId && a.role !== 'introducer');
|
|
100
|
+
const candidateActor = opportunityActors.find(a => a.userId !== discoveryUserId && a.role !== 'introducer');
|
|
101
|
+
if (!sourceActor || !candidateActor) {
|
|
102
|
+
negotiateLog.verbose('Skipping opportunity: no candidateActor found', {
|
|
103
|
+
opportunityId: opp.id,
|
|
104
|
+
discoveryUserId,
|
|
105
|
+
actors: opp.actors?.map(a => ({ userId: a.userId, role: a.role })) ?? [],
|
|
106
|
+
});
|
|
107
|
+
filteredBeforeInvocation.push(opp.id);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return { opp, sourceActor, candidateActor };
|
|
111
|
+
})
|
|
112
|
+
.filter((e) => e !== null);
|
|
113
|
+
await Promise.all(filteredBeforeInvocation.map(compensateTasklessNegotiatingOpportunity));
|
|
114
|
+
negotiateLog.verbose('Candidate filtering complete', {
|
|
115
|
+
inputOpportunities: state.opportunities.length,
|
|
116
|
+
outputCandidates: candidateEntries.length,
|
|
117
|
+
});
|
|
118
|
+
const candidates = await Promise.all(candidateEntries.map(async ({ opp, sourceActor, candidateActor }) => {
|
|
119
|
+
const userId = candidateActor.userId;
|
|
120
|
+
const sourceIntentId = resolveOpportunityActorIntent(sourceActor);
|
|
121
|
+
const candidateIntentId = resolveOpportunityActorIntent(candidateActor);
|
|
122
|
+
const sourceExactActive = sourceIntentId
|
|
123
|
+
? sourceIntentInputs.find((sourceIntent) => sourceIntent.id === sourceIntentId)
|
|
124
|
+
: undefined;
|
|
125
|
+
const [profile, user, activeIntents, intent, sourceIntent] = await Promise.all([
|
|
126
|
+
deps.database.getProfile(userId).catch(() => null),
|
|
127
|
+
deps.database.getUser(userId).catch(() => null),
|
|
128
|
+
includeOtherIntents
|
|
129
|
+
? deps.database.getActiveIntents(userId).catch(() => [])
|
|
130
|
+
: Promise.resolve([]),
|
|
131
|
+
candidateIntentId
|
|
132
|
+
? deps.database.getIntent(candidateIntentId).catch(() => null)
|
|
133
|
+
: null,
|
|
134
|
+
sourceIntentId && !sourceExactActive
|
|
135
|
+
? deps.database.getIntent(sourceIntentId).catch(() => null)
|
|
136
|
+
: null,
|
|
137
|
+
]);
|
|
138
|
+
const ownedFallbackIntent = intent?.userId === userId ? intent : null;
|
|
139
|
+
const ownedSourceIntent = sourceIntent?.userId === discoveryUserId ? sourceIntent : null;
|
|
140
|
+
const candidateIntents = buildPrioritizedNegotiationIntents(activeIntents, candidateIntentId, ownedFallbackIntent, includeOtherIntents);
|
|
141
|
+
return {
|
|
142
|
+
userId,
|
|
143
|
+
...(sourceIntentId ? { sourceIntentId } : {}),
|
|
144
|
+
...(candidateIntentId ? { candidateIntentId } : {}),
|
|
145
|
+
sourceUser: {
|
|
146
|
+
...sourceUser,
|
|
147
|
+
intents: buildPrioritizedNegotiationIntents(sourceIntentInputs, sourceIntentId, ownedSourceIntent, includeOtherIntents),
|
|
148
|
+
},
|
|
149
|
+
opportunityId: opp.id,
|
|
150
|
+
opportunityStatus: opp.status,
|
|
151
|
+
opportunityUpdatedAt: opp.updatedAt,
|
|
152
|
+
reasoning: opp.interpretation?.reasoning ?? '',
|
|
153
|
+
valencyRole: candidateActor.role ?? 'peer',
|
|
154
|
+
networkId: candidateActor.networkId,
|
|
155
|
+
...(state.searchQuery?.trim() && { discoveryQuery: state.searchQuery.trim() }),
|
|
156
|
+
candidateUser: {
|
|
157
|
+
id: userId,
|
|
158
|
+
intents: candidateIntents,
|
|
159
|
+
profile: {
|
|
160
|
+
name: profile?.identity?.name ?? user?.name,
|
|
161
|
+
bio: profile?.identity?.bio ?? user?.intro ?? undefined,
|
|
162
|
+
location: profile?.identity?.location ?? user?.location ?? undefined,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}));
|
|
167
|
+
const isChatPath = !!state.options?.conversationId;
|
|
168
|
+
const maxTurns = isChatPath
|
|
169
|
+
? Number(process.env.NEGOTIATION_MAX_TURNS_CHAT) || 4
|
|
170
|
+
: Number(process.env.NEGOTIATION_MAX_TURNS_AMBIENT) || 6;
|
|
171
|
+
// Fetch per-candidate index context (group by networkId to avoid duplicate lookups)
|
|
172
|
+
const uniqueIndexIds = [...new Set(candidates.map(c => c.networkId).filter((id) => !!id))];
|
|
173
|
+
const indexContextMap = new Map();
|
|
174
|
+
await Promise.all(uniqueIndexIds.map(async (networkId) => {
|
|
175
|
+
const ctx = await deps.database.getNetworkMemberContext(networkId, discoveryUserId).catch(() => null);
|
|
176
|
+
const prompt = [ctx?.indexPrompt, ctx?.memberPrompt]
|
|
177
|
+
.filter((v) => !!v?.trim())
|
|
178
|
+
.join('\n\n');
|
|
179
|
+
if (prompt)
|
|
180
|
+
indexContextMap.set(networkId, prompt);
|
|
181
|
+
}));
|
|
182
|
+
const timeoutMs = await resolveTurnTimeout(isChatPath, uniqueIndexIds, discoveryUserId, deps, candidates.length);
|
|
183
|
+
// Per-candidate hook accumulates negotiation resolutions for discovery
|
|
184
|
+
// question generation, preserving candidate-list order below.
|
|
185
|
+
// Build a stable order index so that resolutions accumulated via the
|
|
186
|
+
// per-candidate async hook can be re-sorted to candidate-list order
|
|
187
|
+
// before being handed to buildQuestionPrompt. Without this the LLM
|
|
188
|
+
// sees negotiations in completion-time order (non-deterministic).
|
|
189
|
+
const candidateOrderById = new Map();
|
|
190
|
+
candidates.forEach((c, i) => candidateOrderById.set(c.userId, i));
|
|
191
|
+
const resolutions = [];
|
|
192
|
+
const resolvedOpportunityIds = new Set();
|
|
193
|
+
const onCandidateResolved = async ({ candidate, turns, outcome }) => {
|
|
194
|
+
if (candidate.opportunityId)
|
|
195
|
+
resolvedOpportunityIds.add(candidate.opportunityId);
|
|
196
|
+
resolutions.push({
|
|
197
|
+
__order: candidateOrderById.get(candidate.userId) ?? Number.MAX_SAFE_INTEGER,
|
|
198
|
+
candidateUserId: candidate.userId,
|
|
199
|
+
counterpartyHint: (() => {
|
|
200
|
+
const bio = candidate.candidateUser.profile?.bio?.trim();
|
|
201
|
+
if (bio)
|
|
202
|
+
return bio;
|
|
203
|
+
return (candidate.candidateUser.profile?.interests ?? []).join(", ");
|
|
204
|
+
})(),
|
|
205
|
+
indexContext: candidate.networkId
|
|
206
|
+
? indexContextMap.get(candidate.networkId) ?? ""
|
|
207
|
+
: "",
|
|
208
|
+
turns,
|
|
209
|
+
outcome,
|
|
210
|
+
});
|
|
211
|
+
if (candidate.opportunityId) {
|
|
212
|
+
await compensateTasklessNegotiatingOpportunity(candidate.opportunityId);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
const negotiationWork = negotiateCandidates(deps.negotiationGraph, sourceUser, candidates, { networkId: '', prompt: '' }, // base context, overridden per-candidate below
|
|
216
|
+
{ maxTurns, traceEmitter: traceEmitter ?? undefined,
|
|
217
|
+
indexContextOverrides: indexContextMap,
|
|
218
|
+
timeoutMs,
|
|
219
|
+
// v2 initiator stamp: every fresh-discovery origin resolves to the
|
|
220
|
+
// discovery user — querying user (chat/tool), intent owner
|
|
221
|
+
// (from-intent), enriched user (from-enrichment/discovery-run), or
|
|
222
|
+
// represented user (from-introducer, via onBehalfOfUserId).
|
|
223
|
+
initiatorUserId: discoveryUserId,
|
|
224
|
+
onCandidateResolved });
|
|
225
|
+
/** Order accumulated resolutions back to candidate-list order. */
|
|
226
|
+
const orderResolutions = () => [...resolutions]
|
|
227
|
+
.sort((a, b) => a.__order - b.__order)
|
|
228
|
+
.map(({ __order: _o, ...r }) => r);
|
|
229
|
+
// Optionally bound this opportunity-matching continuation phase. When
|
|
230
|
+
// the timer wins, return a `timed_out` trace while the unresolved
|
|
231
|
+
// continuation keeps running in the Bun event loop; each candidate's
|
|
232
|
+
// finalize node persists its opportunity status in the DB. Deliberately
|
|
233
|
+
// do NOT await it, abort it, or add unfinished results to
|
|
234
|
+
// state.opportunities: later opportunity reads observe the persisted
|
|
235
|
+
// continuation state. The budget limits this invocation's wait, not
|
|
236
|
+
// background matching work; orphans heal via maintenance scripts or
|
|
237
|
+
// IND-279 when it lands.
|
|
238
|
+
const budgetMs = state.options.negotiateTimeoutMs;
|
|
239
|
+
let acceptedResults;
|
|
240
|
+
if (budgetMs !== undefined) {
|
|
241
|
+
let timerId;
|
|
242
|
+
const timerWork = new Promise((resolve) => {
|
|
243
|
+
timerId = setTimeout(() => resolve(NEGOTIATE_TIMER_SENTINEL), budgetMs);
|
|
244
|
+
});
|
|
245
|
+
// try/finally ensures the timer is cleared on every exit path —
|
|
246
|
+
// sentinel-win, work-win, AND `negotiationWork` rejection. Without
|
|
247
|
+
// this, a rejected negotiation would leave the timer pending and
|
|
248
|
+
// keep the event loop alive until `budgetMs` elapses.
|
|
249
|
+
let raced;
|
|
250
|
+
try {
|
|
251
|
+
raced = await Promise.race([negotiationWork, timerWork]);
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
if (timerId !== undefined)
|
|
255
|
+
clearTimeout(timerId);
|
|
256
|
+
}
|
|
257
|
+
if (raced === NEGOTIATE_TIMER_SENTINEL) {
|
|
258
|
+
// Restore any attempt that is still before its task boundary. A running or
|
|
259
|
+
// parked negotiation already has a task and makes the CAS a no-op; a hung
|
|
260
|
+
// pre-task init becomes owner-actionable while its floating work may retry.
|
|
261
|
+
await Promise.all(candidates
|
|
262
|
+
.filter((candidate) => candidate.opportunityId && !resolvedOpportunityIds.has(candidate.opportunityId))
|
|
263
|
+
.map((candidate) => compensateTasklessNegotiatingOpportunity(candidate.opportunityId)));
|
|
264
|
+
// Floating promise is intentional — see comment above.
|
|
265
|
+
void negotiationWork.catch((err) => {
|
|
266
|
+
negotiateLog.warn('background negotiation failed after timer fired', { error: err });
|
|
267
|
+
});
|
|
268
|
+
negotiateLog.warn('timed out — returning partial results to caller', {
|
|
269
|
+
discoveryUserId,
|
|
270
|
+
candidateCount: candidates.length,
|
|
271
|
+
negotiateTimeoutMs: budgetMs,
|
|
272
|
+
});
|
|
273
|
+
traceEmitter?.({ type: "graph_end", name: "Negotiation graph", durationMs: Date.now() - graphStart });
|
|
274
|
+
const orderedResolutionsPartial = orderResolutions();
|
|
275
|
+
return {
|
|
276
|
+
trace: [{
|
|
277
|
+
node: 'negotiate',
|
|
278
|
+
detail: 'timed_out',
|
|
279
|
+
data: {
|
|
280
|
+
negotiateTimeoutMs: budgetMs,
|
|
281
|
+
candidateCount: candidates.length,
|
|
282
|
+
durationMs: Date.now() - graphStart,
|
|
283
|
+
},
|
|
284
|
+
}],
|
|
285
|
+
discoveryNegotiations: orderedResolutionsPartial.map(toDiscoveryNegotiation),
|
|
286
|
+
discoverySummary: buildDiscoverySummary(orderedResolutionsPartial),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
acceptedResults = raced;
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
acceptedResults = await negotiationWork;
|
|
293
|
+
}
|
|
294
|
+
// No filtering: every candidate's outcome (accept/reject/stalled) was applied to its
|
|
295
|
+
// opportunity row by the negotiation graph's finalize node via the opportunityId we
|
|
296
|
+
// passed. state.opportunities stays as it was at persist time; DB has the new statuses.
|
|
297
|
+
const acceptedUserIds = new Set(acceptedResults.map(r => r.userId));
|
|
298
|
+
const negotiationDurationMs = Date.now() - graphStart;
|
|
299
|
+
const candidateTraceEntries = candidates.map(c => {
|
|
300
|
+
const accepted = acceptedUserIds.has(c.userId);
|
|
301
|
+
const result = accepted ? acceptedResults.find(r => r.userId === c.userId) : null;
|
|
302
|
+
const name = c.candidateUser.profile?.name ?? c.userId;
|
|
303
|
+
const outcome = accepted ? 'accepted' : 'rejected_or_stalled';
|
|
304
|
+
return {
|
|
305
|
+
node: 'negotiate_candidate',
|
|
306
|
+
detail: `${name}: ${outcome}`,
|
|
307
|
+
data: {
|
|
308
|
+
userId: c.userId,
|
|
309
|
+
opportunityId: c.opportunityId,
|
|
310
|
+
name,
|
|
311
|
+
outcome,
|
|
312
|
+
turns: result?.turnCount ?? 0,
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
const acceptedCount = acceptedResults.length;
|
|
317
|
+
const otherCount = candidates.length - acceptedCount;
|
|
318
|
+
const negotiateTrace = [
|
|
319
|
+
{
|
|
320
|
+
node: 'negotiate',
|
|
321
|
+
detail: `${candidates.length} candidate(s) -> ${acceptedCount} accepted, ${otherCount} rejected/stalled`,
|
|
322
|
+
data: {
|
|
323
|
+
durationMs: negotiationDurationMs,
|
|
324
|
+
candidateCount: candidates.length,
|
|
325
|
+
acceptedCount,
|
|
326
|
+
otherCount,
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
...candidateTraceEntries,
|
|
330
|
+
];
|
|
331
|
+
traceEmitter?.({ type: "graph_end", name: "Negotiation graph", durationMs: Date.now() - graphStart });
|
|
332
|
+
const orderedResolutions = orderResolutions();
|
|
333
|
+
return {
|
|
334
|
+
trace: negotiateTrace,
|
|
335
|
+
discoveryNegotiations: orderedResolutions.map(toDiscoveryNegotiation),
|
|
336
|
+
discoverySummary: buildDiscoverySummary(orderedResolutions),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
await Promise.all(state.opportunities.map((opportunity) => compensateTasklessNegotiatingOpportunity(opportunity.id)));
|
|
341
|
+
negotiateLog.error("Negotiation stage failed", { error: err });
|
|
342
|
+
traceEmitter?.({ type: "graph_end", name: "Negotiation graph", durationMs: Date.now() - graphStart });
|
|
343
|
+
return {
|
|
344
|
+
trace: [{
|
|
345
|
+
node: 'negotiate',
|
|
346
|
+
detail: 'Negotiation failed',
|
|
347
|
+
data: { durationMs: Date.now() - graphStart, error: true },
|
|
348
|
+
}],
|
|
349
|
+
discoveryNegotiations: [],
|
|
350
|
+
discoverySummary: buildDiscoverySummary([]),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Decide the per-turn timeout.
|
|
356
|
+
* - Background/queue path (no conversationId): always the park-window
|
|
357
|
+
* budget (AMBIENT_PARK_WINDOW_MS, 5 min). Turns park in
|
|
358
|
+
* `waiting_for_agent` and are picked up via polling; the dispatcher
|
|
359
|
+
* additionally short-circuits to the system agent when no personal
|
|
360
|
+
* agent has a fresh heartbeat (see AgentDispatcherImpl).
|
|
361
|
+
* - Chat path with a personal agent authorized: use the same park-window
|
|
362
|
+
* so the dispatcher parks the turn and the user's personal agent can
|
|
363
|
+
* pick it up via polling.
|
|
364
|
+
* - Chat path with no personal agent: use a short timeout (30s) so the
|
|
365
|
+
* system `Index Negotiator` kicks in without stalling the chat.
|
|
366
|
+
*
|
|
367
|
+
* Check the personal agent per unique candidate network so cross-network
|
|
368
|
+
* chat runs don't get a single authorized agent deciding the timeout for
|
|
369
|
+
* every candidate. Only use the long (polling) timeout when an external
|
|
370
|
+
* (poller) agent is authorized on ALL candidate networks; otherwise fall
|
|
371
|
+
* back to the short timeout so chats don't stall on a network where only
|
|
372
|
+
* the system negotiator is allowed.
|
|
373
|
+
*/
|
|
374
|
+
async function resolveTurnTimeout(isChatPath, uniqueIndexIds, discoveryUserId, deps, candidateCount) {
|
|
375
|
+
const hasExternalAgent = isChatPath && deps.agentDispatcher
|
|
376
|
+
? (uniqueIndexIds.length > 0
|
|
377
|
+
? (await Promise.all(uniqueIndexIds.map((networkId) => deps.agentDispatcher.hasExternalAgent(discoveryUserId, { action: 'manage:negotiations', scopeType: 'network', scopeId: networkId }).catch(() => false)))).every(Boolean)
|
|
378
|
+
: false)
|
|
379
|
+
: false;
|
|
380
|
+
const useLongTimeout = !isChatPath || hasExternalAgent;
|
|
381
|
+
const timeoutMs = useLongTimeout ? AMBIENT_PARK_WINDOW_MS : 30000;
|
|
382
|
+
logger.info('negotiateNode timeout decision', {
|
|
383
|
+
discoveryUserId,
|
|
384
|
+
isChatPath,
|
|
385
|
+
hasDispatcher: !!deps.agentDispatcher,
|
|
386
|
+
hasExternalAgent,
|
|
387
|
+
useLongTimeout,
|
|
388
|
+
timeoutMs,
|
|
389
|
+
candidateCount,
|
|
390
|
+
});
|
|
391
|
+
return timeoutMs;
|
|
392
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discovery pipeline, stage 6: persistence.
|
|
3
|
+
*
|
|
4
|
+
* Three persistence paths share the node — chat introductions, introducer
|
|
5
|
+
* discovery (`onBehalfOfUserId`), and plain discovery. Each one decides whether
|
|
6
|
+
* an existing opportunity between the same people should be reactivated,
|
|
7
|
+
* upgraded, or suppressed before any row is written; only what survives that
|
|
8
|
+
* gate reaches `persistOpportunities`.
|
|
9
|
+
*/
|
|
10
|
+
import type { Id, Opportunity, OpportunityStatus } from '../../shared/interfaces/database.interface.js';
|
|
11
|
+
import { type OpportunityGraphDeps, type OpportunityState } from './opportunity.graph.shared.js';
|
|
12
|
+
/** A pairing that was not persisted because an opportunity already covers it. */
|
|
13
|
+
type ExistingBetweenActor = {
|
|
14
|
+
candidateUserId: Id<'users'>;
|
|
15
|
+
networkId: Id<'networks'>;
|
|
16
|
+
existingOpportunityId?: Id<'opportunities'>;
|
|
17
|
+
existingStatus?: OpportunityStatus;
|
|
18
|
+
reason?: 'same_trigger_recent_duplicate' | 'pair_active_negotiation' | 'final_atomic_conflict';
|
|
19
|
+
existingTriggerIntentId?: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Node 5: Persist
|
|
23
|
+
* Creates opportunities from evaluator-proposed actors (networkId, userId, role, optional intent).
|
|
24
|
+
*/
|
|
25
|
+
export declare function persistNode(state: OpportunityState, deps: OpportunityGraphDeps): Promise<{
|
|
26
|
+
opportunities: never[];
|
|
27
|
+
persistenceOutcome: {
|
|
28
|
+
evaluatedCount: number;
|
|
29
|
+
createdCount: number;
|
|
30
|
+
reactivatedCount: number;
|
|
31
|
+
sameTriggerDuplicateSuppressions: number;
|
|
32
|
+
pairActiveNegotiationSuppressions: number;
|
|
33
|
+
crossTriggerAllowedCount: number;
|
|
34
|
+
finalAtomicConflictCount: number;
|
|
35
|
+
};
|
|
36
|
+
existingBetweenActors?: undefined;
|
|
37
|
+
trace?: undefined;
|
|
38
|
+
error?: undefined;
|
|
39
|
+
} | {
|
|
40
|
+
opportunities: never[];
|
|
41
|
+
persistenceOutcome?: undefined;
|
|
42
|
+
existingBetweenActors?: undefined;
|
|
43
|
+
trace?: undefined;
|
|
44
|
+
error?: undefined;
|
|
45
|
+
} | {
|
|
46
|
+
opportunities: Opportunity[];
|
|
47
|
+
existingBetweenActors: ExistingBetweenActor[];
|
|
48
|
+
persistenceOutcome: {
|
|
49
|
+
evaluatedCount: number;
|
|
50
|
+
createdCount: number;
|
|
51
|
+
reactivatedCount: number;
|
|
52
|
+
sameTriggerDuplicateSuppressions: number;
|
|
53
|
+
pairActiveNegotiationSuppressions: number;
|
|
54
|
+
crossTriggerAllowedCount: number;
|
|
55
|
+
finalAtomicConflictCount: number;
|
|
56
|
+
};
|
|
57
|
+
trace: {
|
|
58
|
+
node: string;
|
|
59
|
+
detail: string;
|
|
60
|
+
data: {
|
|
61
|
+
created: number;
|
|
62
|
+
reactivated: number;
|
|
63
|
+
existingSkipped: number;
|
|
64
|
+
totalOutput: number;
|
|
65
|
+
persistenceOutcome: {
|
|
66
|
+
evaluatedCount: number;
|
|
67
|
+
createdCount: number;
|
|
68
|
+
reactivatedCount: number;
|
|
69
|
+
sameTriggerDuplicateSuppressions: number;
|
|
70
|
+
pairActiveNegotiationSuppressions: number;
|
|
71
|
+
crossTriggerAllowedCount: number;
|
|
72
|
+
finalAtomicConflictCount: number;
|
|
73
|
+
};
|
|
74
|
+
durationMs: number;
|
|
75
|
+
};
|
|
76
|
+
}[];
|
|
77
|
+
error?: undefined;
|
|
78
|
+
} | {
|
|
79
|
+
opportunities: never[];
|
|
80
|
+
existingBetweenActors: never[];
|
|
81
|
+
error: string;
|
|
82
|
+
trace: {
|
|
83
|
+
node: string;
|
|
84
|
+
detail: string;
|
|
85
|
+
data: {
|
|
86
|
+
error: string;
|
|
87
|
+
};
|
|
88
|
+
}[];
|
|
89
|
+
persistenceOutcome?: undefined;
|
|
90
|
+
}>;
|
|
91
|
+
/** Trace summary for {@link persistNode}. */
|
|
92
|
+
export declare function persistTraceSummary(result: unknown): string | undefined;
|
|
93
|
+
export {};
|