@indexnetwork/protocol 6.12.1-rc.404.1 → 6.14.0-rc.406.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.
Files changed (1180) hide show
  1. package/CHANGELOG.md +249 -0
  2. package/IMPLEMENTATION.md +20 -0
  3. package/STABILITY.md +4 -0
  4. package/dist/agent/agent.tools.d.ts +11 -3
  5. package/dist/agent/agent.tools.js +11 -301
  6. package/dist/capabilities/communities.facade.d.ts +13 -0
  7. package/dist/capabilities/communities.facade.js +12 -0
  8. package/dist/capabilities/communities.tools.port.d.ts +5 -0
  9. package/dist/capabilities/communities.tools.port.js +1 -0
  10. package/dist/capabilities/contacts.facade.d.ts +9 -0
  11. package/dist/capabilities/contacts.facade.js +8 -0
  12. package/dist/capabilities/contacts.tools.port.d.ts +9 -0
  13. package/dist/capabilities/contacts.tools.port.js +1 -0
  14. package/dist/capabilities/integrations.facade.d.ts +8 -0
  15. package/dist/capabilities/integrations.facade.js +7 -0
  16. package/dist/capabilities/integrations.tools.port.d.ts +9 -0
  17. package/dist/capabilities/integrations.tools.port.js +1 -0
  18. package/dist/capabilities/interaction-composition.facade.d.ts +10 -0
  19. package/dist/capabilities/interaction-composition.facade.js +9 -0
  20. package/dist/capabilities/negotiation.discovery.facade.d.ts +9 -0
  21. package/dist/capabilities/negotiation.discovery.facade.js +7 -0
  22. package/dist/capabilities/negotiation.facade.d.ts +23 -0
  23. package/dist/capabilities/negotiation.facade.js +17 -0
  24. package/dist/capabilities/negotiation.memory.facade.d.ts +6 -0
  25. package/dist/capabilities/negotiation.memory.facade.js +5 -0
  26. package/dist/capabilities/negotiation.questions.facade.d.ts +6 -0
  27. package/dist/capabilities/negotiation.questions.facade.js +5 -0
  28. package/dist/capabilities/negotiation.state.facade.d.ts +5 -0
  29. package/dist/capabilities/negotiation.state.facade.js +1 -0
  30. package/dist/capabilities/negotiation.summary.facade.d.ts +5 -0
  31. package/dist/capabilities/negotiation.summary.facade.js +5 -0
  32. package/dist/capabilities/negotiation.tools.port.d.ts +5 -0
  33. package/dist/capabilities/negotiation.tools.port.js +1 -0
  34. package/dist/capabilities/opportunities.facade.d.ts +50 -0
  35. package/dist/capabilities/opportunities.facade.js +50 -0
  36. package/dist/capabilities/opportunities.safety.facade.d.ts +2 -0
  37. package/dist/capabilities/opportunities.safety.facade.js +2 -0
  38. package/dist/capabilities/opportunities.tools.port.d.ts +5 -0
  39. package/dist/capabilities/opportunities.tools.port.js +1 -0
  40. package/dist/capabilities/participant-agents.debug.facade.d.ts +2 -0
  41. package/dist/capabilities/participant-agents.debug.facade.js +1 -0
  42. package/dist/capabilities/participant-agents.facade.d.ts +35 -0
  43. package/dist/capabilities/participant-agents.facade.js +35 -0
  44. package/dist/capabilities/participant-agents.tools.facade.d.ts +8 -0
  45. package/dist/capabilities/participant-agents.tools.facade.js +8 -0
  46. package/dist/capabilities/participant-agents.tools.port.d.ts +9 -0
  47. package/dist/capabilities/participant-agents.tools.port.js +1 -0
  48. package/dist/capabilities/participant-context.facade.d.ts +13 -0
  49. package/dist/capabilities/participant-context.facade.js +14 -0
  50. package/dist/capabilities/participant-context.tools.port.d.ts +9 -0
  51. package/dist/capabilities/participant-context.tools.port.js +1 -0
  52. package/dist/capabilities/questions.enqueue.facade.d.ts +5 -0
  53. package/dist/capabilities/questions.enqueue.facade.js +1 -0
  54. package/dist/capabilities/questions.facade.d.ts +8 -0
  55. package/dist/capabilities/questions.facade.js +7 -0
  56. package/dist/capabilities/questions.runtime.facade.d.ts +5 -0
  57. package/dist/capabilities/questions.runtime.facade.js +5 -0
  58. package/dist/capabilities/questions.tools.port.d.ts +5 -0
  59. package/dist/capabilities/questions.tools.port.js +1 -0
  60. package/dist/capabilities/signals.facade.d.ts +16 -0
  61. package/dist/capabilities/signals.facade.js +14 -0
  62. package/dist/capabilities/signals.indexing.facade.d.ts +12 -0
  63. package/dist/capabilities/signals.indexing.facade.js +11 -0
  64. package/dist/capabilities/signals.tools.port.d.ts +5 -0
  65. package/dist/capabilities/signals.tools.port.js +1 -0
  66. package/dist/chat/chat-streaming.types.d.ts +0 -1
  67. package/dist/chat/chat-streaming.types.js +1 -2
  68. package/dist/chat/chat.agent.d.ts +0 -1
  69. package/dist/chat/chat.agent.js +0 -1
  70. package/dist/chat/chat.graph.d.ts +0 -1
  71. package/dist/chat/chat.graph.js +0 -1
  72. package/dist/chat/chat.interrupt.classifier.d.ts +0 -1
  73. package/dist/chat/chat.interrupt.classifier.js +0 -1
  74. package/dist/chat/chat.persona.d.ts +0 -1
  75. package/dist/chat/chat.persona.js +0 -1
  76. package/dist/chat/chat.prompt.d.ts +0 -1
  77. package/dist/chat/chat.prompt.js +0 -1
  78. package/dist/chat/chat.prompt.modules.d.ts +0 -1
  79. package/dist/chat/chat.prompt.modules.js +0 -1
  80. package/dist/chat/chat.question-dedup.d.ts +0 -1
  81. package/dist/chat/chat.question-dedup.js +0 -1
  82. package/dist/chat/chat.state.d.ts +0 -1
  83. package/dist/chat/chat.state.js +0 -1
  84. package/dist/chat/chat.streamer.d.ts +0 -1
  85. package/dist/chat/chat.streamer.js +0 -1
  86. package/dist/chat/chat.suggester.d.ts +0 -1
  87. package/dist/chat/chat.suggester.js +0 -1
  88. package/dist/chat/chat.summarizer.d.ts +0 -1
  89. package/dist/chat/chat.summarizer.js +0 -1
  90. package/dist/chat/chat.title.generator.d.ts +0 -1
  91. package/dist/chat/chat.title.generator.js +0 -1
  92. package/dist/chat/chat.tools.d.ts +0 -1
  93. package/dist/chat/chat.tools.js +0 -1
  94. package/dist/chat/chat.utils.d.ts +0 -1
  95. package/dist/chat/chat.utils.js +0 -1
  96. package/dist/chat/negotiator.persona.d.ts +0 -1
  97. package/dist/chat/negotiator.persona.js +0 -1
  98. package/dist/chat/negotiator.prompt.d.ts +1 -2
  99. package/dist/chat/negotiator.prompt.js +1 -2
  100. package/dist/chat/negotiator.tools.d.ts +0 -1
  101. package/dist/chat/negotiator.tools.js +0 -1
  102. package/dist/chat/onboarding.persona.d.ts +0 -1
  103. package/dist/chat/onboarding.persona.js +0 -1
  104. package/dist/chat/onboarding.prompt.d.ts +0 -1
  105. package/dist/chat/onboarding.prompt.js +0 -1
  106. package/dist/chat/reporter.action.contracts.d.ts +36 -0
  107. package/dist/chat/reporter.action.contracts.js +1 -0
  108. package/dist/chat/reporter.action.tools.d.ts +2 -37
  109. package/dist/chat/reporter.action.tools.js +0 -1
  110. package/dist/chat/reporter.persona.d.ts +0 -1
  111. package/dist/chat/reporter.persona.js +0 -1
  112. package/dist/chat/reporter.prompt.d.ts +0 -1
  113. package/dist/chat/reporter.prompt.js +0 -1
  114. package/dist/chat/signal.persona.d.ts +0 -1
  115. package/dist/chat/signal.persona.js +0 -1
  116. package/dist/chat/signal.prompt.d.ts +0 -1
  117. package/dist/chat/signal.prompt.js +0 -1
  118. package/dist/communities/application/index.d.ts +45 -0
  119. package/dist/communities/application/index.js +50 -0
  120. package/dist/communities/application/indexer.graph.d.ts +446 -0
  121. package/dist/communities/application/indexer.graph.js +420 -0
  122. package/dist/communities/application/indexer.state.d.ts +172 -0
  123. package/dist/communities/application/indexer.state.js +116 -0
  124. package/dist/communities/application/membership.graph.d.ts +270 -0
  125. package/dist/communities/application/membership.graph.js +229 -0
  126. package/dist/communities/application/network.graph.d.ts +648 -0
  127. package/dist/communities/application/network.graph.js +303 -0
  128. package/dist/communities/application/network.recommender.d.ts +44 -0
  129. package/dist/communities/application/network.recommender.js +113 -0
  130. package/dist/communities/application/network.tools.d.ts +35 -0
  131. package/dist/communities/application/network.tools.js +578 -0
  132. package/dist/communities/domain/index.d.ts +29 -0
  133. package/dist/communities/domain/index.js +31 -0
  134. package/dist/communities/domain/membership.state.d.ts +87 -0
  135. package/dist/communities/domain/membership.state.js +53 -0
  136. package/dist/communities/domain/network.state.d.ts +192 -0
  137. package/dist/communities/domain/network.state.js +66 -0
  138. package/dist/communities/index.d.ts +14 -0
  139. package/dist/communities/index.js +14 -0
  140. package/dist/communities/ports/index.d.ts +25 -0
  141. package/dist/communities/ports/index.js +27 -0
  142. package/dist/communities/public/index.d.ts +30 -0
  143. package/dist/communities/public/index.js +33 -0
  144. package/dist/contact/contact.inviter.d.ts +7 -44
  145. package/dist/contact/contact.inviter.js +6 -59
  146. package/dist/contact/contact.tools.d.ts +7 -5
  147. package/dist/contact/contact.tools.js +7 -177
  148. package/dist/contacts/application/contact.inviter.d.ts +49 -0
  149. package/dist/contacts/application/contact.inviter.js +64 -0
  150. package/dist/contacts/application/contact.tools.d.ts +20 -0
  151. package/dist/contacts/application/contact.tools.js +191 -0
  152. package/dist/contacts/application/index.d.ts +22 -0
  153. package/dist/contacts/application/index.js +21 -0
  154. package/dist/contacts/domain/contact.types.d.ts +51 -0
  155. package/dist/contacts/domain/contact.types.js +10 -0
  156. package/dist/contacts/domain/index.d.ts +18 -0
  157. package/dist/contacts/domain/index.js +1 -0
  158. package/dist/contacts/index.d.ts +24 -0
  159. package/dist/contacts/index.js +24 -0
  160. package/dist/contacts/ports/contact.repository.port.d.ts +28 -0
  161. package/dist/contacts/ports/contact.repository.port.js +14 -0
  162. package/dist/contacts/ports/contact.tools.port.d.ts +26 -0
  163. package/dist/contacts/ports/contact.tools.port.js +21 -0
  164. package/dist/contacts/ports/index.d.ts +22 -0
  165. package/dist/contacts/ports/index.js +21 -0
  166. package/dist/contacts/public/index.d.ts +36 -0
  167. package/dist/contacts/public/index.js +33 -0
  168. package/dist/context/context.generator.d.ts +0 -1
  169. package/dist/context/context.generator.js +0 -1
  170. package/dist/enrichment/enrichment.enricher.d.ts +0 -1
  171. package/dist/enrichment/enrichment.enricher.js +0 -1
  172. package/dist/enrichment/enrichment.generator.d.ts +0 -1
  173. package/dist/enrichment/enrichment.generator.js +0 -1
  174. package/dist/enrichment/enrichment.graph.d.ts +6 -7
  175. package/dist/enrichment/enrichment.graph.js +0 -1
  176. package/dist/enrichment/enrichment.state.d.ts +2 -3
  177. package/dist/enrichment/enrichment.state.js +0 -1
  178. package/dist/enrichment/enrichment.tools.d.ts +3 -3
  179. package/dist/enrichment/enrichment.tools.js +0 -1
  180. package/dist/index.d.ts +106 -91
  181. package/dist/index.js +79 -71
  182. package/dist/integration/integration.tools.d.ts +6 -10
  183. package/dist/integration/integration.tools.js +6 -82
  184. package/dist/integrations/application/index.d.ts +14 -0
  185. package/dist/integrations/application/index.js +14 -0
  186. package/dist/integrations/application/integration.tools.d.ts +27 -0
  187. package/dist/integrations/application/integration.tools.js +98 -0
  188. package/dist/integrations/domain/index.d.ts +18 -0
  189. package/dist/integrations/domain/index.js +1 -0
  190. package/dist/integrations/domain/integration.types.d.ts +53 -0
  191. package/dist/integrations/domain/integration.types.js +11 -0
  192. package/dist/integrations/index.d.ts +27 -0
  193. package/dist/integrations/index.js +27 -0
  194. package/dist/integrations/ports/index.d.ts +28 -0
  195. package/dist/integrations/ports/index.js +26 -0
  196. package/dist/integrations/ports/integration.adapter.port.d.ts +62 -0
  197. package/dist/integrations/ports/integration.adapter.port.js +15 -0
  198. package/dist/integrations/ports/integration.importer.port.d.ts +31 -0
  199. package/dist/integrations/ports/integration.importer.port.js +15 -0
  200. package/dist/integrations/ports/integration.tools.port.d.ts +28 -0
  201. package/dist/integrations/ports/integration.tools.port.js +21 -0
  202. package/dist/integrations/public/index.d.ts +37 -0
  203. package/dist/integrations/public/index.js +34 -0
  204. package/dist/intent/intent.clarifier.d.ts +3 -52
  205. package/dist/intent/intent.clarifier.js +2 -202
  206. package/dist/intent/intent.graph.d.ts +3 -551
  207. package/dist/intent/intent.graph.js +3 -937
  208. package/dist/intent/intent.indexer.d.ts +2 -83
  209. package/dist/intent/intent.indexer.js +2 -171
  210. package/dist/intent/intent.inferrer.d.ts +2 -95
  211. package/dist/intent/intent.inferrer.js +2 -234
  212. package/dist/intent/intent.reconciler.d.ts +2 -106
  213. package/dist/intent/intent.reconciler.js +2 -187
  214. package/dist/intent/intent.specificity.d.ts +2 -3
  215. package/dist/intent/intent.specificity.js +2 -3
  216. package/dist/intent/intent.state.d.ts +8 -229
  217. package/dist/intent/intent.state.js +8 -189
  218. package/dist/intent/intent.tools.d.ts +6 -24
  219. package/dist/intent/intent.tools.js +4 -788
  220. package/dist/intent/intent.verifier.d.ts +2 -116
  221. package/dist/intent/intent.verifier.js +2 -263
  222. package/dist/maintenance/maintenance.graph.d.ts +0 -1
  223. package/dist/maintenance/maintenance.graph.js +0 -1
  224. package/dist/maintenance/maintenance.state.d.ts +0 -1
  225. package/dist/maintenance/maintenance.state.js +0 -1
  226. package/dist/mcp/elicitation.builder.d.ts +0 -1
  227. package/dist/mcp/elicitation.builder.js +0 -1
  228. package/dist/mcp/elicitation.dispatcher.d.ts +0 -1
  229. package/dist/mcp/elicitation.dispatcher.js +0 -1
  230. package/dist/mcp/mcp.server.d.ts +0 -1
  231. package/dist/mcp/mcp.server.js +1 -2
  232. package/dist/negotiation/application/index.d.ts +48 -0
  233. package/dist/negotiation/application/index.js +49 -0
  234. package/dist/negotiation/application/insight.generator.d.ts +31 -0
  235. package/dist/negotiation/application/insight.generator.js +105 -0
  236. package/dist/negotiation/application/negotiation.agent.d.ts +110 -0
  237. package/dist/negotiation/application/negotiation.agent.js +244 -0
  238. package/dist/negotiation/application/negotiation.detail-reader.d.ts +94 -0
  239. package/dist/negotiation/application/negotiation.detail-reader.js +91 -0
  240. package/dist/negotiation/application/negotiation.graph.d.ts +666 -0
  241. package/dist/negotiation/application/negotiation.graph.js +1394 -0
  242. package/dist/negotiation/application/negotiation.reflect.d.ts +194 -0
  243. package/dist/negotiation/application/negotiation.reflect.js +145 -0
  244. package/dist/negotiation/application/negotiation.screen.d.ts +78 -0
  245. package/dist/negotiation/application/negotiation.screen.js +108 -0
  246. package/dist/negotiation/application/negotiation.summarizer.d.ts +25 -0
  247. package/dist/negotiation/application/negotiation.summarizer.js +101 -0
  248. package/dist/negotiation/application/negotiation.tools.d.ts +18 -0
  249. package/dist/negotiation/application/negotiation.tools.js +782 -0
  250. package/dist/negotiation/domain/index.d.ts +48 -0
  251. package/dist/negotiation/domain/index.js +50 -0
  252. package/dist/negotiation/domain/negotiation.consultation-policy.d.ts +39 -0
  253. package/dist/negotiation/domain/negotiation.consultation-policy.js +68 -0
  254. package/dist/negotiation/domain/negotiation.deadlock.contracts.d.ts +10 -0
  255. package/dist/negotiation/domain/negotiation.deadlock.contracts.js +1 -0
  256. package/dist/negotiation/domain/negotiation.deadlock.d.ts +81 -0
  257. package/dist/negotiation/domain/negotiation.deadlock.js +103 -0
  258. package/dist/negotiation/domain/negotiation.intent-snapshot-provenance.d.ts +21 -0
  259. package/dist/negotiation/domain/negotiation.intent-snapshot-provenance.js +28 -0
  260. package/dist/negotiation/domain/negotiation.lifecycle-narration.d.ts +18 -0
  261. package/dist/negotiation/domain/negotiation.lifecycle-narration.js +87 -0
  262. package/dist/negotiation/domain/negotiation.memory.d.ts +65 -0
  263. package/dist/negotiation/domain/negotiation.memory.js +83 -0
  264. package/dist/negotiation/domain/negotiation.protocol.d.ts +536 -0
  265. package/dist/negotiation/domain/negotiation.protocol.js +202 -0
  266. package/dist/negotiation/domain/negotiation.question-safety.d.ts +25 -0
  267. package/dist/negotiation/domain/negotiation.question-safety.js +55 -0
  268. package/dist/negotiation/domain/negotiation.screen.contracts.d.ts +88 -0
  269. package/dist/negotiation/domain/negotiation.screen.contracts.js +58 -0
  270. package/dist/negotiation/domain/negotiation.state.d.ts +497 -0
  271. package/dist/negotiation/domain/negotiation.state.js +288 -0
  272. package/dist/negotiation/domain/negotiation.task-lock-policy.d.ts +10 -0
  273. package/dist/negotiation/domain/negotiation.task-lock-policy.js +22 -0
  274. package/dist/negotiation/index.d.ts +49 -0
  275. package/dist/negotiation/index.js +49 -0
  276. package/dist/negotiation/insight.generator.d.ts +3 -30
  277. package/dist/negotiation/insight.generator.js +3 -104
  278. package/dist/negotiation/negotiation.agent.d.ts +3 -109
  279. package/dist/negotiation/negotiation.agent.js +3 -248
  280. package/dist/negotiation/negotiation.attribution.d.ts +0 -1
  281. package/dist/negotiation/negotiation.attribution.js +0 -1
  282. package/dist/negotiation/negotiation.consultation-policy.d.ts +3 -38
  283. package/dist/negotiation/negotiation.consultation-policy.js +3 -67
  284. package/dist/negotiation/negotiation.deadlock.contracts.d.ts +5 -0
  285. package/dist/negotiation/negotiation.deadlock.contracts.js +5 -0
  286. package/dist/negotiation/negotiation.deadlock.d.ts +3 -94
  287. package/dist/negotiation/negotiation.deadlock.js +3 -101
  288. package/dist/negotiation/negotiation.detail-reader.d.ts +5 -0
  289. package/dist/negotiation/negotiation.detail-reader.js +5 -0
  290. package/dist/negotiation/negotiation.graph.d.ts +3 -663
  291. package/dist/negotiation/negotiation.graph.js +3 -1459
  292. package/dist/negotiation/negotiation.intent-snapshot-provenance.d.ts +5 -0
  293. package/dist/negotiation/negotiation.intent-snapshot-provenance.js +5 -0
  294. package/dist/negotiation/negotiation.lifecycle-narration.d.ts +5 -0
  295. package/dist/negotiation/negotiation.lifecycle-narration.js +5 -0
  296. package/dist/negotiation/negotiation.memory.d.ts +5 -56
  297. package/dist/negotiation/negotiation.memory.js +5 -69
  298. package/dist/negotiation/negotiation.protocol.d.ts +3 -535
  299. package/dist/negotiation/negotiation.protocol.js +3 -201
  300. package/dist/negotiation/negotiation.question-safety.d.ts +3 -24
  301. package/dist/negotiation/negotiation.question-safety.js +3 -54
  302. package/dist/negotiation/negotiation.reflect.d.ts +4 -197
  303. package/dist/negotiation/negotiation.reflect.js +4 -151
  304. package/dist/negotiation/negotiation.screen.d.ts +6 -159
  305. package/dist/negotiation/negotiation.screen.js +6 -152
  306. package/dist/negotiation/negotiation.state.d.ts +3 -495
  307. package/dist/negotiation/negotiation.state.js +3 -284
  308. package/dist/negotiation/negotiation.summarizer.d.ts +3 -24
  309. package/dist/negotiation/negotiation.summarizer.js +3 -100
  310. package/dist/negotiation/negotiation.task-lock-policy.d.ts +5 -0
  311. package/dist/negotiation/negotiation.task-lock-policy.js +5 -0
  312. package/dist/negotiation/negotiation.tools.d.ts +3 -15
  313. package/dist/negotiation/negotiation.tools.js +3 -946
  314. package/dist/negotiation/ports/index.d.ts +18 -0
  315. package/dist/negotiation/ports/index.js +18 -0
  316. package/dist/negotiation/ports/negotiation.tools.port.d.ts +18 -0
  317. package/dist/negotiation/ports/negotiation.tools.port.js +1 -0
  318. package/dist/negotiation/public/index.d.ts +58 -0
  319. package/dist/negotiation/public/index.js +60 -0
  320. package/dist/network/indexer/indexer.graph.d.ts +3 -413
  321. package/dist/network/indexer/indexer.graph.js +3 -375
  322. package/dist/network/indexer/indexer.state.d.ts +3 -149
  323. package/dist/network/indexer/indexer.state.js +3 -98
  324. package/dist/network/membership/membership.graph.d.ts +3 -248
  325. package/dist/network/membership/membership.graph.js +3 -202
  326. package/dist/network/membership/membership.state.d.ts +3 -75
  327. package/dist/network/membership/membership.state.js +3 -41
  328. package/dist/network/network.graph.d.ts +3 -629
  329. package/dist/network/network.graph.js +3 -267
  330. package/dist/network/network.recommender.d.ts +3 -40
  331. package/dist/network/network.recommender.js +3 -109
  332. package/dist/network/network.state.d.ts +3 -180
  333. package/dist/network/network.state.js +3 -54
  334. package/dist/network/network.tools.d.ts +5 -3
  335. package/dist/network/network.tools.js +5 -566
  336. package/dist/opportunity/application/delivery-card.cache.d.ts +25 -0
  337. package/dist/opportunity/application/delivery-card.cache.js +43 -0
  338. package/dist/opportunity/application/index.d.ts +63 -0
  339. package/dist/opportunity/application/index.js +68 -0
  340. package/dist/opportunity/application/negotiation-context.loader.d.ts +59 -0
  341. package/dist/opportunity/application/negotiation-context.loader.js +88 -0
  342. package/dist/opportunity/application/opportunity.card-presentation.d.ts +44 -0
  343. package/dist/opportunity/application/opportunity.card-presentation.js +104 -0
  344. package/dist/opportunity/application/opportunity.discover.d.ts +215 -0
  345. package/dist/opportunity/application/opportunity.discover.js +793 -0
  346. package/dist/opportunity/application/opportunity.discovery-continuation-finalization.d.ts +34 -0
  347. package/dist/opportunity/application/opportunity.discovery-continuation-finalization.js +74 -0
  348. package/dist/opportunity/application/opportunity.discovery-mcp-lifecycle-finalization.d.ts +44 -0
  349. package/dist/opportunity/application/opportunity.discovery-mcp-lifecycle-finalization.js +92 -0
  350. package/dist/opportunity/application/opportunity.discovery-negotiation-summary.d.ts +14 -0
  351. package/dist/opportunity/application/opportunity.discovery-negotiation-summary.js +46 -0
  352. package/dist/opportunity/application/opportunity.enricher.d.ts +71 -0
  353. package/dist/opportunity/application/opportunity.enricher.js +280 -0
  354. package/dist/opportunity/application/opportunity.evaluator.d.ts +275 -0
  355. package/dist/opportunity/application/opportunity.evaluator.js +459 -0
  356. package/dist/opportunity/application/opportunity.existing-negotiation.d.ts +79 -0
  357. package/dist/opportunity/application/opportunity.existing-negotiation.js +118 -0
  358. package/dist/opportunity/application/opportunity.feed-selection.d.ts +30 -0
  359. package/dist/opportunity/application/opportunity.feed-selection.js +96 -0
  360. package/dist/opportunity/application/opportunity.graph.d.ts +1427 -0
  361. package/dist/opportunity/application/opportunity.graph.js +3707 -0
  362. package/dist/opportunity/application/opportunity.introducer.d.ts +77 -0
  363. package/dist/opportunity/application/opportunity.introducer.js +99 -0
  364. package/dist/opportunity/application/opportunity.lifecycle.d.ts +63 -0
  365. package/dist/opportunity/application/opportunity.lifecycle.js +181 -0
  366. package/dist/opportunity/application/opportunity.newborn-stamping.d.ts +41 -0
  367. package/dist/opportunity/application/opportunity.newborn-stamping.js +81 -0
  368. package/dist/opportunity/application/opportunity.pending-questions.d.ts +33 -0
  369. package/dist/opportunity/application/opportunity.pending-questions.js +42 -0
  370. package/dist/opportunity/application/opportunity.persist.d.ts +58 -0
  371. package/dist/opportunity/application/opportunity.persist.js +145 -0
  372. package/dist/opportunity/application/opportunity.persistence-admission.d.ts +39 -0
  373. package/dist/opportunity/application/opportunity.persistence-admission.js +59 -0
  374. package/dist/opportunity/application/opportunity.presenter.d.ts +156 -0
  375. package/dist/opportunity/application/opportunity.presenter.js +736 -0
  376. package/dist/opportunity/application/opportunity.tools.d.ts +131 -0
  377. package/dist/opportunity/application/opportunity.tools.js +1834 -0
  378. package/dist/opportunity/application/opportunity.update-admission.d.ts +24 -0
  379. package/dist/opportunity/application/opportunity.update-admission.js +38 -0
  380. package/dist/opportunity/delivery-card.cache.d.ts +5 -26
  381. package/dist/opportunity/delivery-card.cache.js +5 -44
  382. package/dist/opportunity/discovery-question.helper.d.ts +3 -17
  383. package/dist/opportunity/discovery-question.helper.js +3 -14
  384. package/dist/opportunity/discriminator/discriminator.adjustments.d.ts +0 -1
  385. package/dist/opportunity/discriminator/discriminator.adjustments.js +0 -1
  386. package/dist/opportunity/discriminator/discriminator.assigner.d.ts +4 -5
  387. package/dist/opportunity/discriminator/discriminator.assigner.js +0 -1
  388. package/dist/opportunity/discriminator/discriminator.env.d.ts +0 -1
  389. package/dist/opportunity/discriminator/discriminator.env.js +0 -1
  390. package/dist/opportunity/discriminator/discriminator.miner.d.ts +0 -1
  391. package/dist/opportunity/discriminator/discriminator.miner.js +0 -1
  392. package/dist/opportunity/discriminator/discriminator.push.d.ts +0 -1
  393. package/dist/opportunity/discriminator/discriminator.push.js +0 -1
  394. package/dist/opportunity/discriminator/discriminator.question.d.ts +0 -1
  395. package/dist/opportunity/discriminator/discriminator.question.js +0 -1
  396. package/dist/opportunity/discriminator/discriminator.scorer.d.ts +0 -1
  397. package/dist/opportunity/discriminator/discriminator.scorer.js +0 -1
  398. package/dist/opportunity/discriminator/discriminator.shadow.d.ts +0 -1
  399. package/dist/opportunity/discriminator/discriminator.shadow.js +0 -1
  400. package/dist/opportunity/discriminator/discriminator.types.d.ts +0 -1
  401. package/dist/opportunity/discriminator/discriminator.types.js +0 -1
  402. package/dist/opportunity/domain/discovery-question.helper.d.ts +18 -0
  403. package/dist/opportunity/domain/discovery-question.helper.js +15 -0
  404. package/dist/opportunity/domain/index.d.ts +55 -0
  405. package/dist/opportunity/domain/index.js +52 -0
  406. package/dist/opportunity/domain/negotiation-summary.builder.d.ts +37 -0
  407. package/dist/opportunity/domain/negotiation-summary.builder.js +66 -0
  408. package/dist/opportunity/domain/opportunity.actor.d.ts +15 -0
  409. package/dist/opportunity/domain/opportunity.actor.js +39 -0
  410. package/dist/opportunity/domain/opportunity.claim-safety.d.ts +24 -0
  411. package/dist/opportunity/domain/opportunity.claim-safety.js +88 -0
  412. package/dist/opportunity/domain/opportunity.discovery-run-coalescing.d.ts +33 -0
  413. package/dist/opportunity/domain/opportunity.discovery-run-coalescing.js +33 -0
  414. package/dist/opportunity/domain/opportunity.discovery.contracts.d.ts +45 -0
  415. package/dist/opportunity/domain/opportunity.discovery.contracts.js +1 -0
  416. package/dist/opportunity/domain/opportunity.evidence.d.ts +21 -0
  417. package/dist/opportunity/domain/opportunity.evidence.js +78 -0
  418. package/dist/opportunity/domain/opportunity.labels.d.ts +8 -0
  419. package/dist/opportunity/domain/opportunity.labels.js +10 -0
  420. package/dist/opportunity/domain/opportunity.presentation-cache.d.ts +6 -0
  421. package/dist/opportunity/domain/opportunity.presentation-cache.js +14 -0
  422. package/dist/opportunity/domain/opportunity.presentation.d.ts +76 -0
  423. package/dist/opportunity/domain/opportunity.presentation.js +516 -0
  424. package/dist/opportunity/domain/opportunity.safe-presentation.d.ts +104 -0
  425. package/dist/opportunity/domain/opportunity.safe-presentation.js +102 -0
  426. package/dist/opportunity/domain/opportunity.state.d.ts +519 -0
  427. package/dist/opportunity/domain/opportunity.state.js +309 -0
  428. package/dist/opportunity/domain/opportunity.utils.d.ts +217 -0
  429. package/dist/opportunity/domain/opportunity.utils.js +395 -0
  430. package/dist/opportunity/feed/feed.categorizer.d.ts +0 -1
  431. package/dist/opportunity/feed/feed.categorizer.js +0 -1
  432. package/dist/opportunity/feed/feed.graph.d.ts +1 -2
  433. package/dist/opportunity/feed/feed.graph.js +0 -1
  434. package/dist/opportunity/feed/feed.health.d.ts +0 -1
  435. package/dist/opportunity/feed/feed.health.js +0 -1
  436. package/dist/opportunity/feed/feed.state.d.ts +1 -2
  437. package/dist/opportunity/feed/feed.state.js +0 -1
  438. package/dist/opportunity/index.d.ts +37 -0
  439. package/dist/opportunity/index.js +37 -0
  440. package/dist/opportunity/negotiation-context.loader.d.ts +3 -58
  441. package/dist/opportunity/negotiation-context.loader.js +3 -87
  442. package/dist/opportunity/negotiation-evidence/negotiation-evidence.env.d.ts +0 -1
  443. package/dist/opportunity/negotiation-evidence/negotiation-evidence.env.js +0 -1
  444. package/dist/opportunity/negotiation-evidence/negotiation-evidence.extractor.d.ts +0 -1
  445. package/dist/opportunity/negotiation-evidence/negotiation-evidence.extractor.js +0 -1
  446. package/dist/opportunity/negotiation-evidence/negotiation-evidence.miner.d.ts +0 -1
  447. package/dist/opportunity/negotiation-evidence/negotiation-evidence.miner.js +0 -1
  448. package/dist/opportunity/negotiation-evidence/negotiation-evidence.shadow.d.ts +0 -1
  449. package/dist/opportunity/negotiation-evidence/negotiation-evidence.shadow.js +0 -1
  450. package/dist/opportunity/negotiation-evidence/negotiation-evidence.types.d.ts +0 -1
  451. package/dist/opportunity/negotiation-evidence/negotiation-evidence.types.js +0 -1
  452. package/dist/opportunity/negotiation-evidence/negotiation-evidence.verifier.d.ts +0 -1
  453. package/dist/opportunity/negotiation-evidence/negotiation-evidence.verifier.js +0 -1
  454. package/dist/opportunity/negotiation-summary.builder.d.ts +3 -36
  455. package/dist/opportunity/negotiation-summary.builder.js +3 -65
  456. package/dist/opportunity/opportunity.actor.d.ts +3 -14
  457. package/dist/opportunity/opportunity.actor.js +3 -38
  458. package/dist/opportunity/opportunity.card-presentation.d.ts +5 -0
  459. package/dist/opportunity/opportunity.card-presentation.js +5 -0
  460. package/dist/opportunity/opportunity.claim-safety.d.ts +3 -23
  461. package/dist/opportunity/opportunity.claim-safety.js +3 -87
  462. package/dist/opportunity/opportunity.discover.d.ts +3 -218
  463. package/dist/opportunity/opportunity.discover.js +3 -917
  464. package/dist/opportunity/opportunity.discovery-continuation-finalization.d.ts +5 -0
  465. package/dist/opportunity/opportunity.discovery-continuation-finalization.js +5 -0
  466. package/dist/opportunity/opportunity.discovery-mcp-lifecycle-finalization.d.ts +5 -0
  467. package/dist/opportunity/opportunity.discovery-mcp-lifecycle-finalization.js +5 -0
  468. package/dist/opportunity/opportunity.discovery-negotiation-summary.d.ts +5 -0
  469. package/dist/opportunity/opportunity.discovery-negotiation-summary.js +5 -0
  470. package/dist/opportunity/opportunity.discovery-run-coalescing.d.ts +5 -0
  471. package/dist/opportunity/opportunity.discovery-run-coalescing.js +5 -0
  472. package/dist/opportunity/opportunity.discovery.contracts.d.ts +5 -0
  473. package/dist/opportunity/opportunity.discovery.contracts.js +5 -0
  474. package/dist/opportunity/opportunity.enricher.d.ts +3 -70
  475. package/dist/opportunity/opportunity.enricher.js +3 -279
  476. package/dist/opportunity/opportunity.evaluator.d.ts +5 -276
  477. package/dist/opportunity/opportunity.evaluator.js +5 -460
  478. package/dist/opportunity/opportunity.evidence.d.ts +5 -22
  479. package/dist/opportunity/opportunity.evidence.js +5 -79
  480. package/dist/opportunity/opportunity.existing-negotiation.d.ts +5 -0
  481. package/dist/opportunity/opportunity.existing-negotiation.js +5 -0
  482. package/dist/opportunity/opportunity.feed-selection.d.ts +5 -0
  483. package/dist/opportunity/opportunity.feed-selection.js +5 -0
  484. package/dist/opportunity/opportunity.graph.d.ts +3 -1489
  485. package/dist/opportunity/opportunity.graph.js +3 -4071
  486. package/dist/opportunity/opportunity.introducer.d.ts +3 -76
  487. package/dist/opportunity/opportunity.introducer.js +3 -98
  488. package/dist/opportunity/opportunity.labels.d.ts +5 -9
  489. package/dist/opportunity/opportunity.labels.js +5 -11
  490. package/dist/opportunity/opportunity.lifecycle.d.ts +5 -0
  491. package/dist/opportunity/opportunity.lifecycle.js +5 -0
  492. package/dist/opportunity/opportunity.newborn-stamping.d.ts +5 -0
  493. package/dist/opportunity/opportunity.newborn-stamping.js +5 -0
  494. package/dist/opportunity/opportunity.pending-questions.d.ts +3 -32
  495. package/dist/opportunity/opportunity.pending-questions.js +3 -41
  496. package/dist/opportunity/opportunity.persist.d.ts +3 -57
  497. package/dist/opportunity/opportunity.persist.js +3 -144
  498. package/dist/opportunity/opportunity.persistence-admission.d.ts +5 -0
  499. package/dist/opportunity/opportunity.persistence-admission.js +5 -0
  500. package/dist/opportunity/opportunity.presentation-cache.d.ts +5 -7
  501. package/dist/opportunity/opportunity.presentation-cache.js +5 -15
  502. package/dist/opportunity/opportunity.presentation.d.ts +3 -75
  503. package/dist/opportunity/opportunity.presentation.js +3 -515
  504. package/dist/opportunity/opportunity.presenter.d.ts +10 -155
  505. package/dist/opportunity/opportunity.presenter.js +10 -735
  506. package/dist/opportunity/opportunity.safe-presentation.d.ts +3 -103
  507. package/dist/opportunity/opportunity.safe-presentation.js +3 -101
  508. package/dist/opportunity/opportunity.state.d.ts +3 -518
  509. package/dist/opportunity/opportunity.state.js +3 -308
  510. package/dist/opportunity/opportunity.tools.d.ts +3 -157
  511. package/dist/opportunity/opportunity.tools.js +3 -2238
  512. package/dist/opportunity/opportunity.update-admission.d.ts +5 -0
  513. package/dist/opportunity/opportunity.update-admission.js +5 -0
  514. package/dist/opportunity/opportunity.utils.d.ts +3 -216
  515. package/dist/opportunity/opportunity.utils.js +3 -394
  516. package/dist/opportunity/outcome/outcome.env.d.ts +0 -1
  517. package/dist/opportunity/outcome/outcome.env.js +0 -1
  518. package/dist/opportunity/outcome/outcome.hypotheses.d.ts +0 -1
  519. package/dist/opportunity/outcome/outcome.hypotheses.js +0 -1
  520. package/dist/opportunity/outcome/outcome.shadow.d.ts +0 -1
  521. package/dist/opportunity/outcome/outcome.shadow.js +0 -1
  522. package/dist/opportunity/outcome/outcome.types.d.ts +0 -1
  523. package/dist/opportunity/outcome/outcome.types.js +0 -1
  524. package/dist/opportunity/ports/index.d.ts +5 -0
  525. package/dist/opportunity/ports/index.js +1 -0
  526. package/dist/opportunity/ports/opportunity.tools.port.d.ts +5 -0
  527. package/dist/opportunity/ports/opportunity.tools.port.js +1 -0
  528. package/dist/opportunity/public/index.d.ts +58 -0
  529. package/dist/opportunity/public/index.js +51 -0
  530. package/dist/participant-agents/application/agent.tools.d.ts +16 -0
  531. package/dist/participant-agents/application/agent.tools.js +313 -0
  532. package/dist/participant-agents/application/index.d.ts +21 -0
  533. package/dist/participant-agents/application/index.js +22 -0
  534. package/dist/participant-agents/domain/agent.types.d.ts +76 -0
  535. package/dist/participant-agents/domain/agent.types.js +22 -0
  536. package/dist/participant-agents/domain/index.d.ts +20 -0
  537. package/dist/participant-agents/domain/index.js +1 -0
  538. package/dist/participant-agents/index.d.ts +24 -0
  539. package/dist/participant-agents/index.js +24 -0
  540. package/dist/participant-agents/ports/agent.dispatcher.port.d.ts +25 -0
  541. package/dist/participant-agents/ports/agent.dispatcher.port.js +1 -0
  542. package/dist/participant-agents/ports/agent.repository.port.d.ts +118 -0
  543. package/dist/participant-agents/ports/agent.repository.port.js +10 -0
  544. package/dist/participant-agents/ports/agent.tools.port.d.ts +24 -0
  545. package/dist/participant-agents/ports/agent.tools.port.js +20 -0
  546. package/dist/participant-agents/ports/index.d.ts +29 -0
  547. package/dist/participant-agents/ports/index.js +27 -0
  548. package/dist/participant-agents/public/index.d.ts +39 -0
  549. package/dist/participant-agents/public/index.js +36 -0
  550. package/dist/participant-context/application/index.d.ts +73 -0
  551. package/dist/participant-context/application/index.js +83 -0
  552. package/dist/participant-context/domain/index.d.ts +67 -0
  553. package/dist/participant-context/domain/index.js +68 -0
  554. package/dist/participant-context/index.d.ts +16 -0
  555. package/dist/participant-context/index.js +16 -0
  556. package/dist/participant-context/ports/index.d.ts +37 -0
  557. package/dist/participant-context/ports/index.js +34 -0
  558. package/dist/participant-context/public/index.d.ts +48 -0
  559. package/dist/participant-context/public/index.js +55 -0
  560. package/dist/platform/index.d.ts +19 -0
  561. package/dist/platform/index.js +23 -0
  562. package/dist/premise/premise.analyzer.d.ts +0 -1
  563. package/dist/premise/premise.analyzer.js +0 -2
  564. package/dist/premise/premise.decomposer.d.ts +0 -1
  565. package/dist/premise/premise.decomposer.js +0 -2
  566. package/dist/premise/premise.graph.d.ts +1 -2
  567. package/dist/premise/premise.graph.js +0 -2
  568. package/dist/premise/premise.indexer.d.ts +0 -1
  569. package/dist/premise/premise.indexer.js +0 -2
  570. package/dist/premise/premise.state.d.ts +1 -2
  571. package/dist/premise/premise.state.js +0 -1
  572. package/dist/premise/premise.tools.d.ts +3 -3
  573. package/dist/premise/premise.tools.js +0 -2
  574. package/dist/public/index.d.ts +17 -0
  575. package/dist/public/index.js +17 -0
  576. package/dist/questioner/questioner.agent.d.ts +4 -27
  577. package/dist/questioner/questioner.agent.js +3 -133
  578. package/dist/questioner/questioner.ask.tool.d.ts +3 -12
  579. package/dist/questioner/questioner.ask.tool.js +3 -299
  580. package/dist/questioner/questioner.discovery.prompt.d.ts +3 -7
  581. package/dist/questioner/questioner.discovery.prompt.js +3 -180
  582. package/dist/questioner/questioner.env.d.ts +3 -57
  583. package/dist/questioner/questioner.env.js +3 -93
  584. package/dist/questioner/questioner.presets.d.ts +4 -19
  585. package/dist/questioner/questioner.presets.js +3 -465
  586. package/dist/questioner/questioner.qud.d.ts +3 -6
  587. package/dist/questioner/questioner.qud.js +3 -10
  588. package/dist/questioner/questioner.tools.d.ts +3 -15
  589. package/dist/questioner/questioner.tools.js +3 -186
  590. package/dist/questioner/questioner.types.d.ts +4 -218
  591. package/dist/questioner/questioner.types.js +5 -62
  592. package/dist/questions/application/index.d.ts +39 -0
  593. package/dist/questions/application/index.js +41 -0
  594. package/dist/questions/application/question.agent.d.ts +28 -0
  595. package/dist/questions/application/question.agent.js +139 -0
  596. package/dist/questions/application/question.ask.tool.d.ts +12 -0
  597. package/dist/questions/application/question.ask.tool.js +300 -0
  598. package/dist/questions/application/question.discovery.prompt.d.ts +8 -0
  599. package/dist/questions/application/question.discovery.prompt.js +181 -0
  600. package/dist/questions/application/question.env.d.ts +61 -0
  601. package/dist/questions/application/question.env.js +97 -0
  602. package/dist/questions/application/question.input.d.ts +237 -0
  603. package/dist/questions/application/question.input.js +61 -0
  604. package/dist/questions/application/question.presets.d.ts +24 -0
  605. package/dist/questions/application/question.presets.js +445 -0
  606. package/dist/questions/application/question.qud.d.ts +12 -0
  607. package/dist/questions/application/question.qud.js +16 -0
  608. package/dist/questions/application/question.tools.d.ts +15 -0
  609. package/dist/questions/application/question.tools.js +190 -0
  610. package/dist/questions/domain/index.d.ts +25 -0
  611. package/dist/questions/domain/index.js +25 -0
  612. package/dist/questions/domain/question.schema.d.ts +1537 -0
  613. package/dist/questions/domain/question.schema.js +422 -0
  614. package/dist/questions/index.d.ts +22 -0
  615. package/dist/questions/index.js +22 -0
  616. package/dist/questions/ports/index.d.ts +29 -0
  617. package/dist/questions/ports/index.js +27 -0
  618. package/dist/questions/ports/question.generator.port.d.ts +34 -0
  619. package/dist/questions/ports/question.generator.port.js +1 -0
  620. package/dist/questions/ports/question.persistence.port.d.ts +87 -0
  621. package/dist/questions/ports/question.persistence.port.js +1 -0
  622. package/dist/questions/ports/question.tools.port.d.ts +15 -0
  623. package/dist/questions/ports/question.tools.port.js +1 -0
  624. package/dist/questions/public/index.d.ts +47 -0
  625. package/dist/questions/public/index.js +49 -0
  626. package/dist/runtime/background/index.d.ts +14 -0
  627. package/dist/runtime/background/index.js +14 -0
  628. package/dist/runtime/foreground/composition/tool.factory.d.ts +16 -0
  629. package/dist/runtime/foreground/composition/tool.factory.js +224 -0
  630. package/dist/runtime/foreground/composition/tool.registry.d.ts +11 -0
  631. package/dist/runtime/foreground/composition/tool.registry.js +106 -0
  632. package/dist/runtime/foreground/index.d.ts +13 -0
  633. package/dist/runtime/foreground/index.js +13 -0
  634. package/dist/runtime/foreground/signals/intent.tools.d.ts +13 -0
  635. package/dist/runtime/foreground/signals/intent.tools.js +13 -0
  636. package/dist/shared/agent/model-signal.d.ts +0 -1
  637. package/dist/shared/agent/model-signal.js +0 -1
  638. package/dist/shared/agent/model.config.d.ts +0 -1
  639. package/dist/shared/agent/model.config.js +0 -1
  640. package/dist/shared/agent/tool.factory.d.ts +4 -15
  641. package/dist/shared/agent/tool.factory.js +4 -226
  642. package/dist/shared/agent/tool.helpers.d.ts +51 -12
  643. package/dist/shared/agent/tool.helpers.js +0 -1
  644. package/dist/shared/agent/tool.registry.d.ts +9 -9
  645. package/dist/shared/agent/tool.registry.js +9 -104
  646. package/dist/shared/agent/tool.runtime.d.ts +0 -1
  647. package/dist/shared/agent/tool.runtime.js +0 -1
  648. package/dist/shared/agent/tool.scope.d.ts +0 -1
  649. package/dist/shared/agent/tool.scope.js +0 -1
  650. package/dist/shared/agent/utility.tools.d.ts +5 -3
  651. package/dist/shared/agent/utility.tools.js +0 -1
  652. package/dist/shared/assignment/network-assignment.policy.d.ts +0 -1
  653. package/dist/shared/assignment/network-assignment.policy.js +0 -1
  654. package/dist/shared/hyde/hyde.documents.d.ts +0 -1
  655. package/dist/shared/hyde/hyde.documents.js +0 -1
  656. package/dist/shared/hyde/hyde.env.d.ts +0 -1
  657. package/dist/shared/hyde/hyde.env.js +0 -1
  658. package/dist/shared/hyde/hyde.frame.d.ts +4 -5
  659. package/dist/shared/hyde/hyde.frame.js +0 -1
  660. package/dist/shared/hyde/hyde.generator.d.ts +0 -1
  661. package/dist/shared/hyde/hyde.generator.js +0 -1
  662. package/dist/shared/hyde/hyde.graph.d.ts +0 -1
  663. package/dist/shared/hyde/hyde.graph.js +0 -1
  664. package/dist/shared/hyde/hyde.state.d.ts +0 -1
  665. package/dist/shared/hyde/hyde.state.js +0 -1
  666. package/dist/shared/hyde/hyde.strategies.d.ts +0 -1
  667. package/dist/shared/hyde/hyde.strategies.js +0 -1
  668. package/dist/shared/hyde/hyde.validator.d.ts +0 -1
  669. package/dist/shared/hyde/hyde.validator.js +0 -1
  670. package/dist/shared/hyde/lens.inferrer.d.ts +6 -7
  671. package/dist/shared/hyde/lens.inferrer.js +0 -1
  672. package/dist/shared/interfaces/agent-dispatcher.interface.d.ts +4 -1
  673. package/dist/shared/interfaces/agent-dispatcher.interface.js +4 -1
  674. package/dist/shared/interfaces/agent.interface.d.ts +13 -173
  675. package/dist/shared/interfaces/agent.interface.js +12 -12
  676. package/dist/shared/interfaces/auth.interface.d.ts +0 -1
  677. package/dist/shared/interfaces/auth.interface.js +0 -1
  678. package/dist/shared/interfaces/cache.interface.d.ts +0 -1
  679. package/dist/shared/interfaces/cache.interface.js +0 -1
  680. package/dist/shared/interfaces/chat-message-writer.interface.d.ts +0 -1
  681. package/dist/shared/interfaces/chat-message-writer.interface.js +0 -1
  682. package/dist/shared/interfaces/chat-session.interface.d.ts +0 -1
  683. package/dist/shared/interfaces/chat-session.interface.js +0 -1
  684. package/dist/shared/interfaces/chat-summary.interface.d.ts +0 -1
  685. package/dist/shared/interfaces/chat-summary.interface.js +0 -1
  686. package/dist/shared/interfaces/connect-link.interface.d.ts +0 -1
  687. package/dist/shared/interfaces/connect-link.interface.js +0 -1
  688. package/dist/shared/interfaces/contact.interface.d.ts +7 -55
  689. package/dist/shared/interfaces/contact.interface.js +7 -1
  690. package/dist/shared/interfaces/database.interface.d.ts +0 -1
  691. package/dist/shared/interfaces/database.interface.js +0 -1
  692. package/dist/shared/interfaces/delivery-ledger.interface.d.ts +0 -1
  693. package/dist/shared/interfaces/delivery-ledger.interface.js +0 -1
  694. package/dist/shared/interfaces/discovery-run.interface.d.ts +2 -3
  695. package/dist/shared/interfaces/discovery-run.interface.js +0 -1
  696. package/dist/shared/interfaces/embedder.interface.d.ts +0 -1
  697. package/dist/shared/interfaces/embedder.interface.js +0 -1
  698. package/dist/shared/interfaces/enrichment-run.interface.d.ts +2 -3
  699. package/dist/shared/interfaces/enrichment-run.interface.js +0 -1
  700. package/dist/shared/interfaces/enrichment.interface.d.ts +0 -1
  701. package/dist/shared/interfaces/enrichment.interface.js +0 -1
  702. package/dist/shared/interfaces/integration.interface.d.ts +6 -88
  703. package/dist/shared/interfaces/integration.interface.js +7 -1
  704. package/dist/shared/interfaces/negotiation-events.interface.d.ts +0 -1
  705. package/dist/shared/interfaces/negotiation-events.interface.js +0 -1
  706. package/dist/shared/interfaces/negotiation-summary.interface.d.ts +0 -1
  707. package/dist/shared/interfaces/negotiation-summary.interface.js +0 -1
  708. package/dist/shared/interfaces/negotiator-memory.interface.d.ts +0 -1
  709. package/dist/shared/interfaces/negotiator-memory.interface.js +0 -1
  710. package/dist/shared/interfaces/question-generator.interface.d.ts +3 -28
  711. package/dist/shared/interfaces/question-generator.interface.js +0 -1
  712. package/dist/shared/interfaces/questioner.interface.d.ts +3 -77
  713. package/dist/shared/interfaces/questioner.interface.js +0 -1
  714. package/dist/shared/interfaces/queue.interface.d.ts +0 -1
  715. package/dist/shared/interfaces/queue.interface.js +0 -1
  716. package/dist/shared/interfaces/scraper.interface.d.ts +0 -1
  717. package/dist/shared/interfaces/scraper.interface.js +0 -1
  718. package/dist/shared/interfaces/tool-request-context.interface.d.ts +12 -0
  719. package/dist/shared/interfaces/tool-request-context.interface.js +1 -0
  720. package/dist/shared/network/metadata.renderer.d.ts +0 -1
  721. package/dist/shared/network/metadata.renderer.js +0 -1
  722. package/dist/shared/observability/debug-meta.sanitizer.d.ts +0 -1
  723. package/dist/shared/observability/debug-meta.sanitizer.js +0 -1
  724. package/dist/shared/observability/log.d.ts +0 -1
  725. package/dist/shared/observability/log.js +0 -1
  726. package/dist/shared/observability/performance.d.ts +0 -1
  727. package/dist/shared/observability/performance.js +0 -1
  728. package/dist/shared/observability/protocol.logger.d.ts +0 -1
  729. package/dist/shared/observability/protocol.logger.js +0 -1
  730. package/dist/shared/observability/request-context.d.ts +0 -1
  731. package/dist/shared/observability/request-context.js +0 -1
  732. package/dist/shared/observability/trace.d.ts +0 -1
  733. package/dist/shared/observability/trace.js +0 -1
  734. package/dist/shared/schemas/chat-context.schema.d.ts +0 -1
  735. package/dist/shared/schemas/chat-context.schema.js +0 -1
  736. package/dist/shared/schemas/discovery-question.schema.d.ts +17 -18
  737. package/dist/shared/schemas/discovery-question.schema.js +0 -1
  738. package/dist/shared/schemas/identity.schema.d.ts +0 -1
  739. package/dist/shared/schemas/identity.schema.js +0 -1
  740. package/dist/shared/schemas/mcp-auth.schema.d.ts +0 -1
  741. package/dist/shared/schemas/mcp-auth.schema.js +0 -1
  742. package/dist/shared/schemas/negotiation-digest.schema.d.ts +5 -6
  743. package/dist/shared/schemas/negotiation-digest.schema.js +0 -1
  744. package/dist/shared/schemas/negotiation-state.schema.d.ts +4 -5
  745. package/dist/shared/schemas/negotiation-state.schema.js +0 -1
  746. package/dist/shared/schemas/network-assignment.schema.d.ts +0 -1
  747. package/dist/shared/schemas/network-assignment.schema.js +0 -1
  748. package/dist/shared/schemas/pending-question.schema.d.ts +0 -1
  749. package/dist/shared/schemas/pending-question.schema.js +0 -1
  750. package/dist/shared/schemas/question.schema.d.ts +3 -1535
  751. package/dist/shared/schemas/question.schema.js +3 -420
  752. package/dist/shared/ui/lucide.icon-catalog.d.ts +0 -1
  753. package/dist/shared/ui/lucide.icon-catalog.js +0 -1
  754. package/dist/shared/utils/social-label.d.ts +0 -1
  755. package/dist/shared/utils/social-label.js +0 -1
  756. package/dist/shared/utils/telegram-handle.d.ts +0 -1
  757. package/dist/shared/utils/telegram-handle.js +0 -1
  758. package/dist/signals/application/index.d.ts +19 -0
  759. package/dist/signals/application/index.js +22 -0
  760. package/dist/signals/application/intent.clarifier.d.ts +51 -0
  761. package/dist/signals/application/intent.clarifier.js +201 -0
  762. package/dist/signals/application/intent.graph.d.ts +552 -0
  763. package/dist/signals/application/intent.graph.js +938 -0
  764. package/dist/signals/application/intent.indexer.d.ts +82 -0
  765. package/dist/signals/application/intent.indexer.js +170 -0
  766. package/dist/signals/application/intent.inferrer.d.ts +94 -0
  767. package/dist/signals/application/intent.inferrer.js +233 -0
  768. package/dist/signals/application/intent.reconciler.d.ts +105 -0
  769. package/dist/signals/application/intent.reconciler.js +186 -0
  770. package/dist/signals/application/intent.tools.d.ts +24 -0
  771. package/dist/signals/application/intent.tools.js +789 -0
  772. package/dist/signals/application/intent.verifier.d.ts +115 -0
  773. package/dist/signals/application/intent.verifier.js +262 -0
  774. package/dist/signals/domain/index.d.ts +9 -0
  775. package/dist/signals/domain/index.js +11 -0
  776. package/dist/signals/domain/intent.state.d.ts +230 -0
  777. package/dist/signals/domain/intent.state.js +190 -0
  778. package/dist/signals/domain/signal.specificity.d.ts +2 -0
  779. package/dist/signals/domain/signal.specificity.js +2 -0
  780. package/dist/signals/index.d.ts +13 -0
  781. package/dist/signals/index.js +13 -0
  782. package/dist/signals/ports/index.d.ts +15 -0
  783. package/dist/signals/ports/index.js +13 -0
  784. package/dist/signals/public/index.d.ts +15 -0
  785. package/dist/signals/public/index.js +16 -0
  786. package/package.json +14 -3
  787. package/dist/agent/agent.tools.d.ts.map +0 -1
  788. package/dist/agent/agent.tools.js.map +0 -1
  789. package/dist/agent/tests/fakes.d.ts +0 -25
  790. package/dist/agent/tests/fakes.d.ts.map +0 -1
  791. package/dist/agent/tests/fakes.js +0 -178
  792. package/dist/agent/tests/fakes.js.map +0 -1
  793. package/dist/chat/chat-streaming.types.d.ts.map +0 -1
  794. package/dist/chat/chat-streaming.types.js.map +0 -1
  795. package/dist/chat/chat.agent.d.ts.map +0 -1
  796. package/dist/chat/chat.agent.js.map +0 -1
  797. package/dist/chat/chat.graph.d.ts.map +0 -1
  798. package/dist/chat/chat.graph.js.map +0 -1
  799. package/dist/chat/chat.interrupt.classifier.d.ts.map +0 -1
  800. package/dist/chat/chat.interrupt.classifier.js.map +0 -1
  801. package/dist/chat/chat.persona.d.ts.map +0 -1
  802. package/dist/chat/chat.persona.js.map +0 -1
  803. package/dist/chat/chat.prompt.d.ts.map +0 -1
  804. package/dist/chat/chat.prompt.js.map +0 -1
  805. package/dist/chat/chat.prompt.modules.d.ts.map +0 -1
  806. package/dist/chat/chat.prompt.modules.js.map +0 -1
  807. package/dist/chat/chat.question-dedup.d.ts.map +0 -1
  808. package/dist/chat/chat.question-dedup.js.map +0 -1
  809. package/dist/chat/chat.state.d.ts.map +0 -1
  810. package/dist/chat/chat.state.js.map +0 -1
  811. package/dist/chat/chat.streamer.d.ts.map +0 -1
  812. package/dist/chat/chat.streamer.js.map +0 -1
  813. package/dist/chat/chat.suggester.d.ts.map +0 -1
  814. package/dist/chat/chat.suggester.js.map +0 -1
  815. package/dist/chat/chat.summarizer.d.ts.map +0 -1
  816. package/dist/chat/chat.summarizer.js.map +0 -1
  817. package/dist/chat/chat.title.generator.d.ts.map +0 -1
  818. package/dist/chat/chat.title.generator.js.map +0 -1
  819. package/dist/chat/chat.tools.d.ts.map +0 -1
  820. package/dist/chat/chat.tools.js.map +0 -1
  821. package/dist/chat/chat.utils.d.ts.map +0 -1
  822. package/dist/chat/chat.utils.js.map +0 -1
  823. package/dist/chat/negotiator.persona.d.ts.map +0 -1
  824. package/dist/chat/negotiator.persona.js.map +0 -1
  825. package/dist/chat/negotiator.prompt.d.ts.map +0 -1
  826. package/dist/chat/negotiator.prompt.js.map +0 -1
  827. package/dist/chat/negotiator.tools.d.ts.map +0 -1
  828. package/dist/chat/negotiator.tools.js.map +0 -1
  829. package/dist/chat/onboarding.persona.d.ts.map +0 -1
  830. package/dist/chat/onboarding.persona.js.map +0 -1
  831. package/dist/chat/onboarding.prompt.d.ts.map +0 -1
  832. package/dist/chat/onboarding.prompt.js.map +0 -1
  833. package/dist/chat/reporter.action.tools.d.ts.map +0 -1
  834. package/dist/chat/reporter.action.tools.js.map +0 -1
  835. package/dist/chat/reporter.persona.d.ts.map +0 -1
  836. package/dist/chat/reporter.persona.js.map +0 -1
  837. package/dist/chat/reporter.prompt.d.ts.map +0 -1
  838. package/dist/chat/reporter.prompt.js.map +0 -1
  839. package/dist/chat/signal.persona.d.ts.map +0 -1
  840. package/dist/chat/signal.persona.js.map +0 -1
  841. package/dist/chat/signal.prompt.d.ts.map +0 -1
  842. package/dist/chat/signal.prompt.js.map +0 -1
  843. package/dist/chat/tests/chat.graph.mocks.d.ts +0 -98
  844. package/dist/chat/tests/chat.graph.mocks.d.ts.map +0 -1
  845. package/dist/chat/tests/chat.graph.mocks.js +0 -225
  846. package/dist/chat/tests/chat.graph.mocks.js.map +0 -1
  847. package/dist/contact/contact.inviter.d.ts.map +0 -1
  848. package/dist/contact/contact.inviter.js.map +0 -1
  849. package/dist/contact/contact.tools.d.ts.map +0 -1
  850. package/dist/contact/contact.tools.js.map +0 -1
  851. package/dist/context/context.generator.d.ts.map +0 -1
  852. package/dist/context/context.generator.js.map +0 -1
  853. package/dist/enrichment/enrichment.enricher.d.ts.map +0 -1
  854. package/dist/enrichment/enrichment.enricher.js.map +0 -1
  855. package/dist/enrichment/enrichment.generator.d.ts.map +0 -1
  856. package/dist/enrichment/enrichment.generator.js.map +0 -1
  857. package/dist/enrichment/enrichment.graph.d.ts.map +0 -1
  858. package/dist/enrichment/enrichment.graph.js.map +0 -1
  859. package/dist/enrichment/enrichment.state.d.ts.map +0 -1
  860. package/dist/enrichment/enrichment.state.js.map +0 -1
  861. package/dist/enrichment/enrichment.tools.d.ts.map +0 -1
  862. package/dist/enrichment/enrichment.tools.js.map +0 -1
  863. package/dist/index.d.ts.map +0 -1
  864. package/dist/index.js.map +0 -1
  865. package/dist/integration/integration.tools.d.ts.map +0 -1
  866. package/dist/integration/integration.tools.js.map +0 -1
  867. package/dist/intent/intent.clarifier.d.ts.map +0 -1
  868. package/dist/intent/intent.clarifier.js.map +0 -1
  869. package/dist/intent/intent.graph.d.ts.map +0 -1
  870. package/dist/intent/intent.graph.js.map +0 -1
  871. package/dist/intent/intent.indexer.d.ts.map +0 -1
  872. package/dist/intent/intent.indexer.js.map +0 -1
  873. package/dist/intent/intent.inferrer.d.ts.map +0 -1
  874. package/dist/intent/intent.inferrer.js.map +0 -1
  875. package/dist/intent/intent.reconciler.d.ts.map +0 -1
  876. package/dist/intent/intent.reconciler.js.map +0 -1
  877. package/dist/intent/intent.specificity.d.ts.map +0 -1
  878. package/dist/intent/intent.specificity.js.map +0 -1
  879. package/dist/intent/intent.state.d.ts.map +0 -1
  880. package/dist/intent/intent.state.js.map +0 -1
  881. package/dist/intent/intent.tools.d.ts.map +0 -1
  882. package/dist/intent/intent.tools.js.map +0 -1
  883. package/dist/intent/intent.verifier.d.ts.map +0 -1
  884. package/dist/intent/intent.verifier.js.map +0 -1
  885. package/dist/maintenance/maintenance.graph.d.ts.map +0 -1
  886. package/dist/maintenance/maintenance.graph.js.map +0 -1
  887. package/dist/maintenance/maintenance.state.d.ts.map +0 -1
  888. package/dist/maintenance/maintenance.state.js.map +0 -1
  889. package/dist/mcp/elicitation.builder.d.ts.map +0 -1
  890. package/dist/mcp/elicitation.builder.js.map +0 -1
  891. package/dist/mcp/elicitation.dispatcher.d.ts.map +0 -1
  892. package/dist/mcp/elicitation.dispatcher.js.map +0 -1
  893. package/dist/mcp/mcp.server.d.ts.map +0 -1
  894. package/dist/mcp/mcp.server.js.map +0 -1
  895. package/dist/negotiation/insight.generator.d.ts.map +0 -1
  896. package/dist/negotiation/insight.generator.js.map +0 -1
  897. package/dist/negotiation/negotiation.agent.d.ts.map +0 -1
  898. package/dist/negotiation/negotiation.agent.js.map +0 -1
  899. package/dist/negotiation/negotiation.attribution.d.ts.map +0 -1
  900. package/dist/negotiation/negotiation.attribution.js.map +0 -1
  901. package/dist/negotiation/negotiation.consultation-policy.d.ts.map +0 -1
  902. package/dist/negotiation/negotiation.consultation-policy.js.map +0 -1
  903. package/dist/negotiation/negotiation.deadlock.d.ts.map +0 -1
  904. package/dist/negotiation/negotiation.deadlock.js.map +0 -1
  905. package/dist/negotiation/negotiation.graph.d.ts.map +0 -1
  906. package/dist/negotiation/negotiation.graph.js.map +0 -1
  907. package/dist/negotiation/negotiation.memory.d.ts.map +0 -1
  908. package/dist/negotiation/negotiation.memory.js.map +0 -1
  909. package/dist/negotiation/negotiation.protocol.d.ts.map +0 -1
  910. package/dist/negotiation/negotiation.protocol.js.map +0 -1
  911. package/dist/negotiation/negotiation.question-safety.d.ts.map +0 -1
  912. package/dist/negotiation/negotiation.question-safety.js.map +0 -1
  913. package/dist/negotiation/negotiation.reflect.d.ts.map +0 -1
  914. package/dist/negotiation/negotiation.reflect.js.map +0 -1
  915. package/dist/negotiation/negotiation.screen.d.ts.map +0 -1
  916. package/dist/negotiation/negotiation.screen.js.map +0 -1
  917. package/dist/negotiation/negotiation.state.d.ts.map +0 -1
  918. package/dist/negotiation/negotiation.state.js.map +0 -1
  919. package/dist/negotiation/negotiation.summarizer.d.ts.map +0 -1
  920. package/dist/negotiation/negotiation.summarizer.js.map +0 -1
  921. package/dist/negotiation/negotiation.tools.d.ts.map +0 -1
  922. package/dist/negotiation/negotiation.tools.js.map +0 -1
  923. package/dist/network/indexer/indexer.graph.d.ts.map +0 -1
  924. package/dist/network/indexer/indexer.graph.js.map +0 -1
  925. package/dist/network/indexer/indexer.state.d.ts.map +0 -1
  926. package/dist/network/indexer/indexer.state.js.map +0 -1
  927. package/dist/network/membership/membership.graph.d.ts.map +0 -1
  928. package/dist/network/membership/membership.graph.js.map +0 -1
  929. package/dist/network/membership/membership.state.d.ts.map +0 -1
  930. package/dist/network/membership/membership.state.js.map +0 -1
  931. package/dist/network/network.graph.d.ts.map +0 -1
  932. package/dist/network/network.graph.js.map +0 -1
  933. package/dist/network/network.recommender.d.ts.map +0 -1
  934. package/dist/network/network.recommender.js.map +0 -1
  935. package/dist/network/network.state.d.ts.map +0 -1
  936. package/dist/network/network.state.js.map +0 -1
  937. package/dist/network/network.tools.d.ts.map +0 -1
  938. package/dist/network/network.tools.js.map +0 -1
  939. package/dist/opportunity/delivery-card.cache.d.ts.map +0 -1
  940. package/dist/opportunity/delivery-card.cache.js.map +0 -1
  941. package/dist/opportunity/discovery-question.helper.d.ts.map +0 -1
  942. package/dist/opportunity/discovery-question.helper.js.map +0 -1
  943. package/dist/opportunity/discriminator/discriminator.adjustments.d.ts.map +0 -1
  944. package/dist/opportunity/discriminator/discriminator.adjustments.js.map +0 -1
  945. package/dist/opportunity/discriminator/discriminator.assigner.d.ts.map +0 -1
  946. package/dist/opportunity/discriminator/discriminator.assigner.js.map +0 -1
  947. package/dist/opportunity/discriminator/discriminator.env.d.ts.map +0 -1
  948. package/dist/opportunity/discriminator/discriminator.env.js.map +0 -1
  949. package/dist/opportunity/discriminator/discriminator.miner.d.ts.map +0 -1
  950. package/dist/opportunity/discriminator/discriminator.miner.js.map +0 -1
  951. package/dist/opportunity/discriminator/discriminator.push.d.ts.map +0 -1
  952. package/dist/opportunity/discriminator/discriminator.push.js.map +0 -1
  953. package/dist/opportunity/discriminator/discriminator.question.d.ts.map +0 -1
  954. package/dist/opportunity/discriminator/discriminator.question.js.map +0 -1
  955. package/dist/opportunity/discriminator/discriminator.scorer.d.ts.map +0 -1
  956. package/dist/opportunity/discriminator/discriminator.scorer.js.map +0 -1
  957. package/dist/opportunity/discriminator/discriminator.shadow.d.ts.map +0 -1
  958. package/dist/opportunity/discriminator/discriminator.shadow.js.map +0 -1
  959. package/dist/opportunity/discriminator/discriminator.types.d.ts.map +0 -1
  960. package/dist/opportunity/discriminator/discriminator.types.js.map +0 -1
  961. package/dist/opportunity/feed/feed.categorizer.d.ts.map +0 -1
  962. package/dist/opportunity/feed/feed.categorizer.js.map +0 -1
  963. package/dist/opportunity/feed/feed.graph.d.ts.map +0 -1
  964. package/dist/opportunity/feed/feed.graph.js.map +0 -1
  965. package/dist/opportunity/feed/feed.health.d.ts.map +0 -1
  966. package/dist/opportunity/feed/feed.health.js.map +0 -1
  967. package/dist/opportunity/feed/feed.state.d.ts.map +0 -1
  968. package/dist/opportunity/feed/feed.state.js.map +0 -1
  969. package/dist/opportunity/negotiation-context.loader.d.ts.map +0 -1
  970. package/dist/opportunity/negotiation-context.loader.js.map +0 -1
  971. package/dist/opportunity/negotiation-evidence/negotiation-evidence.env.d.ts.map +0 -1
  972. package/dist/opportunity/negotiation-evidence/negotiation-evidence.env.js.map +0 -1
  973. package/dist/opportunity/negotiation-evidence/negotiation-evidence.extractor.d.ts.map +0 -1
  974. package/dist/opportunity/negotiation-evidence/negotiation-evidence.extractor.js.map +0 -1
  975. package/dist/opportunity/negotiation-evidence/negotiation-evidence.miner.d.ts.map +0 -1
  976. package/dist/opportunity/negotiation-evidence/negotiation-evidence.miner.js.map +0 -1
  977. package/dist/opportunity/negotiation-evidence/negotiation-evidence.shadow.d.ts.map +0 -1
  978. package/dist/opportunity/negotiation-evidence/negotiation-evidence.shadow.js.map +0 -1
  979. package/dist/opportunity/negotiation-evidence/negotiation-evidence.types.d.ts.map +0 -1
  980. package/dist/opportunity/negotiation-evidence/negotiation-evidence.types.js.map +0 -1
  981. package/dist/opportunity/negotiation-evidence/negotiation-evidence.verifier.d.ts.map +0 -1
  982. package/dist/opportunity/negotiation-evidence/negotiation-evidence.verifier.js.map +0 -1
  983. package/dist/opportunity/negotiation-summary.builder.d.ts.map +0 -1
  984. package/dist/opportunity/negotiation-summary.builder.js.map +0 -1
  985. package/dist/opportunity/opportunity.actor.d.ts.map +0 -1
  986. package/dist/opportunity/opportunity.actor.js.map +0 -1
  987. package/dist/opportunity/opportunity.claim-safety.d.ts.map +0 -1
  988. package/dist/opportunity/opportunity.claim-safety.js.map +0 -1
  989. package/dist/opportunity/opportunity.discover.d.ts.map +0 -1
  990. package/dist/opportunity/opportunity.discover.js.map +0 -1
  991. package/dist/opportunity/opportunity.enricher.d.ts.map +0 -1
  992. package/dist/opportunity/opportunity.enricher.js.map +0 -1
  993. package/dist/opportunity/opportunity.evaluator.d.ts.map +0 -1
  994. package/dist/opportunity/opportunity.evaluator.js.map +0 -1
  995. package/dist/opportunity/opportunity.evidence.d.ts.map +0 -1
  996. package/dist/opportunity/opportunity.evidence.js.map +0 -1
  997. package/dist/opportunity/opportunity.graph.d.ts.map +0 -1
  998. package/dist/opportunity/opportunity.graph.js.map +0 -1
  999. package/dist/opportunity/opportunity.introducer.d.ts.map +0 -1
  1000. package/dist/opportunity/opportunity.introducer.js.map +0 -1
  1001. package/dist/opportunity/opportunity.labels.d.ts.map +0 -1
  1002. package/dist/opportunity/opportunity.labels.js.map +0 -1
  1003. package/dist/opportunity/opportunity.pending-questions.d.ts.map +0 -1
  1004. package/dist/opportunity/opportunity.pending-questions.js.map +0 -1
  1005. package/dist/opportunity/opportunity.persist.d.ts.map +0 -1
  1006. package/dist/opportunity/opportunity.persist.js.map +0 -1
  1007. package/dist/opportunity/opportunity.presentation-cache.d.ts.map +0 -1
  1008. package/dist/opportunity/opportunity.presentation-cache.js.map +0 -1
  1009. package/dist/opportunity/opportunity.presentation.d.ts.map +0 -1
  1010. package/dist/opportunity/opportunity.presentation.js.map +0 -1
  1011. package/dist/opportunity/opportunity.presenter.d.ts.map +0 -1
  1012. package/dist/opportunity/opportunity.presenter.js.map +0 -1
  1013. package/dist/opportunity/opportunity.safe-presentation.d.ts.map +0 -1
  1014. package/dist/opportunity/opportunity.safe-presentation.js.map +0 -1
  1015. package/dist/opportunity/opportunity.state.d.ts.map +0 -1
  1016. package/dist/opportunity/opportunity.state.js.map +0 -1
  1017. package/dist/opportunity/opportunity.tools.d.ts.map +0 -1
  1018. package/dist/opportunity/opportunity.tools.js.map +0 -1
  1019. package/dist/opportunity/opportunity.utils.d.ts.map +0 -1
  1020. package/dist/opportunity/opportunity.utils.js.map +0 -1
  1021. package/dist/opportunity/outcome/outcome.env.d.ts.map +0 -1
  1022. package/dist/opportunity/outcome/outcome.env.js.map +0 -1
  1023. package/dist/opportunity/outcome/outcome.hypotheses.d.ts.map +0 -1
  1024. package/dist/opportunity/outcome/outcome.hypotheses.js.map +0 -1
  1025. package/dist/opportunity/outcome/outcome.shadow.d.ts.map +0 -1
  1026. package/dist/opportunity/outcome/outcome.shadow.js.map +0 -1
  1027. package/dist/opportunity/outcome/outcome.types.d.ts.map +0 -1
  1028. package/dist/opportunity/outcome/outcome.types.js.map +0 -1
  1029. package/dist/premise/premise.analyzer.d.ts.map +0 -1
  1030. package/dist/premise/premise.analyzer.js.map +0 -1
  1031. package/dist/premise/premise.decomposer.d.ts.map +0 -1
  1032. package/dist/premise/premise.decomposer.js.map +0 -1
  1033. package/dist/premise/premise.graph.d.ts.map +0 -1
  1034. package/dist/premise/premise.graph.js.map +0 -1
  1035. package/dist/premise/premise.indexer.d.ts.map +0 -1
  1036. package/dist/premise/premise.indexer.js.map +0 -1
  1037. package/dist/premise/premise.state.d.ts.map +0 -1
  1038. package/dist/premise/premise.state.js.map +0 -1
  1039. package/dist/premise/premise.tools.d.ts.map +0 -1
  1040. package/dist/premise/premise.tools.js.map +0 -1
  1041. package/dist/questioner/questioner.agent.d.ts.map +0 -1
  1042. package/dist/questioner/questioner.agent.js.map +0 -1
  1043. package/dist/questioner/questioner.ask.tool.d.ts.map +0 -1
  1044. package/dist/questioner/questioner.ask.tool.js.map +0 -1
  1045. package/dist/questioner/questioner.discovery.prompt.d.ts.map +0 -1
  1046. package/dist/questioner/questioner.discovery.prompt.js.map +0 -1
  1047. package/dist/questioner/questioner.env.d.ts.map +0 -1
  1048. package/dist/questioner/questioner.env.js.map +0 -1
  1049. package/dist/questioner/questioner.presets.d.ts.map +0 -1
  1050. package/dist/questioner/questioner.presets.js.map +0 -1
  1051. package/dist/questioner/questioner.qud.d.ts.map +0 -1
  1052. package/dist/questioner/questioner.qud.js.map +0 -1
  1053. package/dist/questioner/questioner.tools.d.ts.map +0 -1
  1054. package/dist/questioner/questioner.tools.js.map +0 -1
  1055. package/dist/questioner/questioner.types.d.ts.map +0 -1
  1056. package/dist/questioner/questioner.types.js.map +0 -1
  1057. package/dist/shared/agent/model-signal.d.ts.map +0 -1
  1058. package/dist/shared/agent/model-signal.js.map +0 -1
  1059. package/dist/shared/agent/model.config.d.ts.map +0 -1
  1060. package/dist/shared/agent/model.config.js.map +0 -1
  1061. package/dist/shared/agent/tests/llm-assert.d.ts +0 -11
  1062. package/dist/shared/agent/tests/llm-assert.d.ts.map +0 -1
  1063. package/dist/shared/agent/tests/llm-assert.js +0 -43
  1064. package/dist/shared/agent/tests/llm-assert.js.map +0 -1
  1065. package/dist/shared/agent/tool.factory.d.ts.map +0 -1
  1066. package/dist/shared/agent/tool.factory.js.map +0 -1
  1067. package/dist/shared/agent/tool.helpers.d.ts.map +0 -1
  1068. package/dist/shared/agent/tool.helpers.js.map +0 -1
  1069. package/dist/shared/agent/tool.registry.d.ts.map +0 -1
  1070. package/dist/shared/agent/tool.registry.js.map +0 -1
  1071. package/dist/shared/agent/tool.runtime.d.ts.map +0 -1
  1072. package/dist/shared/agent/tool.runtime.js.map +0 -1
  1073. package/dist/shared/agent/tool.scope.d.ts.map +0 -1
  1074. package/dist/shared/agent/tool.scope.js.map +0 -1
  1075. package/dist/shared/agent/utility.tools.d.ts.map +0 -1
  1076. package/dist/shared/agent/utility.tools.js.map +0 -1
  1077. package/dist/shared/assignment/network-assignment.policy.d.ts.map +0 -1
  1078. package/dist/shared/assignment/network-assignment.policy.js.map +0 -1
  1079. package/dist/shared/hyde/hyde.documents.d.ts.map +0 -1
  1080. package/dist/shared/hyde/hyde.documents.js.map +0 -1
  1081. package/dist/shared/hyde/hyde.env.d.ts.map +0 -1
  1082. package/dist/shared/hyde/hyde.env.js.map +0 -1
  1083. package/dist/shared/hyde/hyde.frame.d.ts.map +0 -1
  1084. package/dist/shared/hyde/hyde.frame.js.map +0 -1
  1085. package/dist/shared/hyde/hyde.generator.d.ts.map +0 -1
  1086. package/dist/shared/hyde/hyde.generator.js.map +0 -1
  1087. package/dist/shared/hyde/hyde.graph.d.ts.map +0 -1
  1088. package/dist/shared/hyde/hyde.graph.js.map +0 -1
  1089. package/dist/shared/hyde/hyde.state.d.ts.map +0 -1
  1090. package/dist/shared/hyde/hyde.state.js.map +0 -1
  1091. package/dist/shared/hyde/hyde.strategies.d.ts.map +0 -1
  1092. package/dist/shared/hyde/hyde.strategies.js.map +0 -1
  1093. package/dist/shared/hyde/hyde.validator.d.ts.map +0 -1
  1094. package/dist/shared/hyde/hyde.validator.js.map +0 -1
  1095. package/dist/shared/hyde/lens.inferrer.d.ts.map +0 -1
  1096. package/dist/shared/hyde/lens.inferrer.js.map +0 -1
  1097. package/dist/shared/interfaces/agent-dispatcher.interface.d.ts.map +0 -1
  1098. package/dist/shared/interfaces/agent-dispatcher.interface.js.map +0 -1
  1099. package/dist/shared/interfaces/agent.interface.d.ts.map +0 -1
  1100. package/dist/shared/interfaces/agent.interface.js.map +0 -1
  1101. package/dist/shared/interfaces/auth.interface.d.ts.map +0 -1
  1102. package/dist/shared/interfaces/auth.interface.js.map +0 -1
  1103. package/dist/shared/interfaces/cache.interface.d.ts.map +0 -1
  1104. package/dist/shared/interfaces/cache.interface.js.map +0 -1
  1105. package/dist/shared/interfaces/chat-message-writer.interface.d.ts.map +0 -1
  1106. package/dist/shared/interfaces/chat-message-writer.interface.js.map +0 -1
  1107. package/dist/shared/interfaces/chat-session.interface.d.ts.map +0 -1
  1108. package/dist/shared/interfaces/chat-session.interface.js.map +0 -1
  1109. package/dist/shared/interfaces/chat-summary.interface.d.ts.map +0 -1
  1110. package/dist/shared/interfaces/chat-summary.interface.js.map +0 -1
  1111. package/dist/shared/interfaces/connect-link.interface.d.ts.map +0 -1
  1112. package/dist/shared/interfaces/connect-link.interface.js.map +0 -1
  1113. package/dist/shared/interfaces/contact.interface.d.ts.map +0 -1
  1114. package/dist/shared/interfaces/contact.interface.js.map +0 -1
  1115. package/dist/shared/interfaces/database.interface.d.ts.map +0 -1
  1116. package/dist/shared/interfaces/database.interface.js.map +0 -1
  1117. package/dist/shared/interfaces/delivery-ledger.interface.d.ts.map +0 -1
  1118. package/dist/shared/interfaces/delivery-ledger.interface.js.map +0 -1
  1119. package/dist/shared/interfaces/discovery-run.interface.d.ts.map +0 -1
  1120. package/dist/shared/interfaces/discovery-run.interface.js.map +0 -1
  1121. package/dist/shared/interfaces/embedder.interface.d.ts.map +0 -1
  1122. package/dist/shared/interfaces/embedder.interface.js.map +0 -1
  1123. package/dist/shared/interfaces/enrichment-run.interface.d.ts.map +0 -1
  1124. package/dist/shared/interfaces/enrichment-run.interface.js.map +0 -1
  1125. package/dist/shared/interfaces/enrichment.interface.d.ts.map +0 -1
  1126. package/dist/shared/interfaces/enrichment.interface.js.map +0 -1
  1127. package/dist/shared/interfaces/integration.interface.d.ts.map +0 -1
  1128. package/dist/shared/interfaces/integration.interface.js.map +0 -1
  1129. package/dist/shared/interfaces/negotiation-events.interface.d.ts.map +0 -1
  1130. package/dist/shared/interfaces/negotiation-events.interface.js.map +0 -1
  1131. package/dist/shared/interfaces/negotiation-summary.interface.d.ts.map +0 -1
  1132. package/dist/shared/interfaces/negotiation-summary.interface.js.map +0 -1
  1133. package/dist/shared/interfaces/negotiator-memory.interface.d.ts.map +0 -1
  1134. package/dist/shared/interfaces/negotiator-memory.interface.js.map +0 -1
  1135. package/dist/shared/interfaces/question-generator.interface.d.ts.map +0 -1
  1136. package/dist/shared/interfaces/question-generator.interface.js.map +0 -1
  1137. package/dist/shared/interfaces/questioner.interface.d.ts.map +0 -1
  1138. package/dist/shared/interfaces/questioner.interface.js.map +0 -1
  1139. package/dist/shared/interfaces/queue.interface.d.ts.map +0 -1
  1140. package/dist/shared/interfaces/queue.interface.js.map +0 -1
  1141. package/dist/shared/interfaces/scraper.interface.d.ts.map +0 -1
  1142. package/dist/shared/interfaces/scraper.interface.js.map +0 -1
  1143. package/dist/shared/network/metadata.renderer.d.ts.map +0 -1
  1144. package/dist/shared/network/metadata.renderer.js.map +0 -1
  1145. package/dist/shared/observability/debug-meta.sanitizer.d.ts.map +0 -1
  1146. package/dist/shared/observability/debug-meta.sanitizer.js.map +0 -1
  1147. package/dist/shared/observability/log.d.ts.map +0 -1
  1148. package/dist/shared/observability/log.js.map +0 -1
  1149. package/dist/shared/observability/performance.d.ts.map +0 -1
  1150. package/dist/shared/observability/performance.js.map +0 -1
  1151. package/dist/shared/observability/protocol.logger.d.ts.map +0 -1
  1152. package/dist/shared/observability/protocol.logger.js.map +0 -1
  1153. package/dist/shared/observability/request-context.d.ts.map +0 -1
  1154. package/dist/shared/observability/request-context.js.map +0 -1
  1155. package/dist/shared/observability/trace.d.ts.map +0 -1
  1156. package/dist/shared/observability/trace.js.map +0 -1
  1157. package/dist/shared/schemas/chat-context.schema.d.ts.map +0 -1
  1158. package/dist/shared/schemas/chat-context.schema.js.map +0 -1
  1159. package/dist/shared/schemas/discovery-question.schema.d.ts.map +0 -1
  1160. package/dist/shared/schemas/discovery-question.schema.js.map +0 -1
  1161. package/dist/shared/schemas/identity.schema.d.ts.map +0 -1
  1162. package/dist/shared/schemas/identity.schema.js.map +0 -1
  1163. package/dist/shared/schemas/mcp-auth.schema.d.ts.map +0 -1
  1164. package/dist/shared/schemas/mcp-auth.schema.js.map +0 -1
  1165. package/dist/shared/schemas/negotiation-digest.schema.d.ts.map +0 -1
  1166. package/dist/shared/schemas/negotiation-digest.schema.js.map +0 -1
  1167. package/dist/shared/schemas/negotiation-state.schema.d.ts.map +0 -1
  1168. package/dist/shared/schemas/negotiation-state.schema.js.map +0 -1
  1169. package/dist/shared/schemas/network-assignment.schema.d.ts.map +0 -1
  1170. package/dist/shared/schemas/network-assignment.schema.js.map +0 -1
  1171. package/dist/shared/schemas/pending-question.schema.d.ts.map +0 -1
  1172. package/dist/shared/schemas/pending-question.schema.js.map +0 -1
  1173. package/dist/shared/schemas/question.schema.d.ts.map +0 -1
  1174. package/dist/shared/schemas/question.schema.js.map +0 -1
  1175. package/dist/shared/ui/lucide.icon-catalog.d.ts.map +0 -1
  1176. package/dist/shared/ui/lucide.icon-catalog.js.map +0 -1
  1177. package/dist/shared/utils/social-label.d.ts.map +0 -1
  1178. package/dist/shared/utils/social-label.js.map +0 -1
  1179. package/dist/shared/utils/telegram-handle.d.ts.map +0 -1
  1180. package/dist/shared/utils/telegram-handle.js.map +0 -1
@@ -0,0 +1,3707 @@
1
+ /**
2
+ * Opportunity Graph: Linear Multi-Step Workflow for Opportunity Discovery
3
+ *
4
+ * Architecture: Follows intent graph pattern with Annotation-based state.
5
+ * Flow: Prep → Scope → Discovery → Evaluation → Ranking → Persist → END
6
+ *
7
+ * Key Constraints:
8
+ * - Opportunities only between intents sharing the same index
9
+ * - Both intents must have hyde documents for semantic matching
10
+ * - Non-indexed intents cannot participate in discovery
11
+ *
12
+ * Constructor injects Database, Embedder, and compiled HyDE graph.
13
+ */
14
+ import { StateGraph, START, END } from '@langchain/langgraph';
15
+ import { OpportunityGraphState } from '../domain/opportunity.state.js';
16
+ import { resolveInitialStatus } from '../domain/opportunity.state.js';
17
+ import { OpportunityEvaluator } from './opportunity.evaluator.js';
18
+ import { IntentIndexer } from '../../capabilities/signals.indexing.facade.js';
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';
22
+ import { validateOpportunityActors } from '../domain/opportunity.utils.js';
23
+ import { safeFallbackSummary } from '../domain/opportunity.safe-presentation.js';
24
+ import { hasUnsupportedOpportunityClaim } from '../domain/opportunity.claim-safety.js';
25
+ import { persistOpportunities } from './opportunity.persist.js';
26
+ import { INTRODUCER_DISCOVERY_SOURCE } from './opportunity.introducer.js';
27
+ import { negotiateCandidates, ASK_USER_LOCK_SLACK_MS, askUserAnswerWindowMs, AMBIENT_PARK_WINDOW_MS } from "../../capabilities/negotiation.discovery.facade.js";
28
+ import { buildDiscoverySummary, toDiscoveryNegotiation } from "../domain/negotiation-summary.builder.js";
29
+ import { protocolLogger, withCallLogging } from '../../shared/observability/protocol.logger.js';
30
+ import { timed } from '../../shared/observability/performance.js';
31
+ import { renderNetworkContext } from '../../shared/network/metadata.renderer.js';
32
+ import { requestContext } from "../../shared/observability/request-context.js";
33
+ import { mergeOpportunityEvidence, withCandidateEvidence, withMatchedStrategies } from '../domain/opportunity.evidence.js';
34
+ import { normalizeOpportunityActorIntent, resolveOpportunityActorIntent } from '../domain/opportunity.actor.js';
35
+ import { approveOpportunityIntroduction, deleteOpportunityLifecycle, sendOpportunityLifecycle, updateOpportunityLifecycle } from './opportunity.lifecycle.js';
36
+ import { stampEligibleNewbornOpportunities } from './opportunity.newborn-stamping.js';
37
+ import { buildPrioritizedNegotiationIntents, negotiateExistingOpportunity, negotiationIncludesOtherIntents } from './opportunity.existing-negotiation.js';
38
+ import { admitOpportunityPersistence, createEligibleOpportunityStatusUpdater } from './opportunity.persistence-admission.js';
39
+ export { buildPrioritizedNegotiationIntents } from './opportunity.existing-negotiation.js';
40
+ const logger = protocolLogger('OpportunityGraph');
41
+ const prepLog = protocolLogger('OpportunityGraph:Prep');
42
+ const scopeLog = protocolLogger('OpportunityGraph:Scope');
43
+ const resolveLog = protocolLogger('OpportunityGraph:Resolve');
44
+ const discoveryLog = protocolLogger('OpportunityGraph:Discovery');
45
+ const evaluationLog = protocolLogger('OpportunityGraph:Evaluation');
46
+ const negotiateLog = protocolLogger('OpportunityGraph:Negotiate');
47
+ const rankingLog = protocolLogger('OpportunityGraph:Ranking');
48
+ const introValidationLog = protocolLogger('OpportunityGraph:IntroValidation');
49
+ const introEvaluationLog = protocolLogger('OpportunityGraph:IntroEvaluation');
50
+ const persistLog = protocolLogger('OpportunityGraph:Persist');
51
+ const persistPathLog = protocolLogger('OpportunityGraph:Persist:PathSelect');
52
+ const persistDedupLog = protocolLogger('OpportunityGraph:Persist:Dedup');
53
+ const readLog = protocolLogger('OpportunityGraph:Read');
54
+ const updateLog = protocolLogger('OpportunityGraph:Update');
55
+ const deleteLog = protocolLogger('OpportunityGraph:Delete');
56
+ const sendLog = protocolLogger('OpportunityGraph:Send');
57
+ const negotiateExistingLog = protocolLogger('OpportunityGraph:NegotiateExisting');
58
+ const routingLog = protocolLogger('OpportunityGraph:Routing');
59
+ /** 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). */
60
+ const DEDUP_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
61
+ const ACTIVE_NEGOTIATION_TASK_STATES = new Set([
62
+ 'submitted',
63
+ 'working',
64
+ 'input_required',
65
+ 'waiting_for_agent',
66
+ 'claimed',
67
+ ]);
68
+ function isActiveNegotiationTaskFresh(task) {
69
+ if (!ACTIVE_NEGOTIATION_TASK_STATES.has(task.state))
70
+ return false;
71
+ const freshnessMs = task.state === 'input_required'
72
+ ? askUserAnswerWindowMs() + ASK_USER_LOCK_SLACK_MS
73
+ : 5 * 60 * 1000;
74
+ return Date.now() - new Date(task.updatedAt).getTime() < freshnessMs;
75
+ }
76
+ function triggerForOwner(opportunity, ownerUserId) {
77
+ return opportunity.detection.triggeredBy
78
+ ?? opportunity.actors.find((actor) => actor.userId === ownerUserId)?.intent;
79
+ }
80
+ function belongsToOwnedIntent(opportunity, ownerUserId, triggerIntentId) {
81
+ return opportunity.detection.triggeredBy === triggerIntentId
82
+ || opportunity.actors.some((actor) => actor.userId === ownerUserId && actor.intent === triggerIntentId);
83
+ }
84
+ /**
85
+ * IND-567: Cool-down window (ms) for cross-query rejection suppression.
86
+ * Candidates with a recently rejected or stalled opportunity within this window
87
+ * receive a similarity penalty during evaluation ranking. Default 7 days.
88
+ * Override with DISCOVERY_REJECTION_COOLDOWN_DAYS (positive float).
89
+ */
90
+ const DEFAULT_REJECTION_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000;
91
+ function getRejectionCooldownMs() {
92
+ const raw = process.env.DISCOVERY_REJECTION_COOLDOWN_DAYS;
93
+ if (!raw)
94
+ return DEFAULT_REJECTION_COOLDOWN_MS;
95
+ const n = Number.parseFloat(raw);
96
+ return Number.isFinite(n) && n > 0 ? Math.round(n * 24 * 60 * 60 * 1000) : DEFAULT_REJECTION_COOLDOWN_MS;
97
+ }
98
+ /**
99
+ * Similarity multiplier applied to candidates that fall within the rejection
100
+ * cool-down window (IND-567). 0.5 halves their ranking score, typically
101
+ * pushing them below the evaluation-batch cut while leaving a soft trace in
102
+ * the trace log rather than silently dropping them.
103
+ */
104
+ const REJECTION_COOLDOWN_SIMILARITY_PENALTY = 0.5;
105
+ /** Default cap for source premises used by premise-to-premise discovery. Prevents BACKEND-5-style fan-out. */
106
+ const DEFAULT_SOURCE_PREMISE_DISCOVERY_LIMIT = 40;
107
+ function networkMembershipPairKey(userId, networkId) {
108
+ return `${userId}\u0000${networkId}`;
109
+ }
110
+ /** Per-source cap for candidate premise matches. */
111
+ const PREMISE_MATCH_LIMIT_PER_SOURCE = 20;
112
+ /** Resolve the source premise discovery cap from env, preserving 0 as an explicit disable switch. */
113
+ function getSourcePremiseDiscoveryLimit() {
114
+ const raw = process.env.DISCOVERY_SOURCE_PREMISE_LIMIT;
115
+ if (raw === undefined || raw.trim() === '')
116
+ return DEFAULT_SOURCE_PREMISE_DISCOVERY_LIMIT;
117
+ const parsed = Number.parseInt(raw, 10);
118
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_SOURCE_PREMISE_DISCOVERY_LIMIT;
119
+ }
120
+ function buildEvaluatorEvidenceKey(candidate) {
121
+ return [
122
+ candidate.candidateUserId,
123
+ candidate.networkId,
124
+ candidate.candidateIntentId ?? candidate.candidatePremiseId ?? candidate.sourceContextId ?? 'profile',
125
+ ].join(':');
126
+ }
127
+ /**
128
+ * Builds a compact text summary of the discoverer's profile and active intents
129
+ * for use as profileContext in HyDE generation.
130
+ * @param profile - The discoverer's profile data (identity, attributes)
131
+ * @param intents - The discoverer's indexed intents (capped at 5)
132
+ * @returns A context string, or undefined if no meaningful data is available
133
+ */
134
+ export function buildDiscovererContext(profile, intents) {
135
+ const lines = [];
136
+ if (profile) {
137
+ const identity = profile.identity;
138
+ if (identity?.name || identity?.bio) {
139
+ lines.push(`Profile: ${[identity.name, identity.bio].filter(Boolean).join(', ')}`);
140
+ }
141
+ if (identity?.location) {
142
+ lines.push(`Location: ${identity.location}`);
143
+ }
144
+ if (profile.context) {
145
+ lines.push(`Context: ${profile.context}`);
146
+ }
147
+ }
148
+ if (intents?.length) {
149
+ // indexedIntents preserves DB order from getActiveIntents (newest first),
150
+ // so slice(0, 5) is deterministic without an explicit sort.
151
+ const capped = intents.slice(0, 5);
152
+ lines.push('');
153
+ lines.push('Active intents:');
154
+ for (const intent of capped) {
155
+ lines.push(`- ${intent.payload}`);
156
+ }
157
+ }
158
+ return lines.length > 0 ? lines.join('\n') : undefined;
159
+ }
160
+ /**
161
+ * Build a networkContexts map for the evaluator from a set of entities.
162
+ * Fetches network data, checks permissions.contextInjection.discovery,
163
+ * and renders context for eligible networks.
164
+ */
165
+ async function buildNetworkContexts(entities, database) {
166
+ const networkIds = [...new Set(entities.map((e) => e.networkId))];
167
+ const networks = await Promise.all(networkIds.map((nid) => database.getNetwork(nid).then((n) => ({ nid, n }))));
168
+ const contexts = {};
169
+ for (const { nid, n: network } of networks) {
170
+ if (!network)
171
+ continue;
172
+ const perms = (network.permissions ?? {});
173
+ const injection = perms.contextInjection;
174
+ if (injection?.discovery === false)
175
+ continue;
176
+ contexts[nid] = renderNetworkContext({
177
+ type: network.type ?? 'community',
178
+ title: network.title,
179
+ prompt: network.prompt,
180
+ metadata: network.metadata ?? {},
181
+ });
182
+ }
183
+ return contexts;
184
+ }
185
+ /**
186
+ * Factory class to build and compile the Opportunity Graph.
187
+ * Uses dependency injection for testability.
188
+ */
189
+ export class OpportunityGraphFactory {
190
+ constructor(database, embedder, hydeGenerator, optionalEvaluator, queueNotification, negotiationGraph,
191
+ /**
192
+ * Used on the chat path to decide whether to wait for the user's personal
193
+ * agent (long timeout) or fall back to the system agent immediately
194
+ * (short timeout). Without it, the chat path always uses a short timeout.
195
+ */
196
+ agentDispatcher,
197
+ /**
198
+ * Callback to enqueue a negotiate_existing job for an opportunity.
199
+ * When provided, negotiate_existing mode uses this to queue follow-up
200
+ * negotiations after introducer approval.
201
+ */
202
+ queueNegotiateExisting,
203
+ /** Host-side P4b stamper. Omitted by manual/introducer/enrichment roots. */
204
+ stampNewbornOpportunities) {
205
+ this.database = database;
206
+ this.embedder = embedder;
207
+ this.hydeGenerator = hydeGenerator;
208
+ this.optionalEvaluator = optionalEvaluator;
209
+ this.queueNotification = queueNotification;
210
+ this.negotiationGraph = negotiationGraph;
211
+ this.agentDispatcher = agentDispatcher;
212
+ this.queueNegotiateExisting = queueNegotiateExisting;
213
+ this.stampNewbornOpportunities = stampNewbornOpportunities;
214
+ }
215
+ createGraph() {
216
+ const evaluatorAgent = this.optionalEvaluator ?? new OpportunityEvaluator();
217
+ // ═══════════════════════════════════════════════════════════════
218
+ // NODE DEFINITIONS
219
+ // ═══════════════════════════════════════════════════════════════
220
+ /**
221
+ * Wraps a graph node function to emit agent_start/agent_end trace events
222
+ * at its boundaries so the frontend TRACE panel shows real-time progress.
223
+ * @param traceName - Kebab-case agent name (e.g. "opportunity-prep")
224
+ * @param nodeFn - The original node function
225
+ * @param summaryFn - Optional function to derive a summary string from the node result
226
+ */
227
+ function withNodeTrace(traceName, nodeFn, summaryFn) {
228
+ return async (state) => {
229
+ const traceEmitter = requestContext.getStore()?.traceEmitter;
230
+ const nodeStart = Date.now();
231
+ traceEmitter?.({ type: "agent_start", name: traceName });
232
+ try {
233
+ const result = await nodeFn(state);
234
+ const durationMs = Date.now() - nodeStart;
235
+ const summary = summaryFn?.(result) ?? undefined;
236
+ traceEmitter?.({ type: "agent_end", name: traceName, durationMs, summary });
237
+ return result;
238
+ }
239
+ catch (err) {
240
+ const durationMs = Date.now() - nodeStart;
241
+ const errMsg = err instanceof Error ? err.message : String(err);
242
+ traceEmitter?.({ type: "agent_end", name: traceName, durationMs, summary: `error: ${errMsg}` });
243
+ throw err;
244
+ }
245
+ };
246
+ }
247
+ /**
248
+ * Node 0: Prep
249
+ * Fetches user's network memberships and validates requirements.
250
+ * Returns empty if user has no network memberships (requirement).
251
+ */
252
+ const prepNode = withNodeTrace("opportunity-prep", async (state) => timed("OpportunityGraph.prep", async () => withCallLogging(prepLog, 'prepNode', {
253
+ userId: state.userId,
254
+ hasSearchQuery: !!state.searchQuery,
255
+ requestedIndexId: state.networkId ?? undefined,
256
+ }, async () => {
257
+ // Use getNetworkMemberships (all memberships) for search scope — NOT getUserIndexIds
258
+ // (which filters by autoAssign=true and is intended only for intent assignment).
259
+ const memberships = await this.database.getNetworkMemberships(state.userId);
260
+ const userNetworkIds = memberships.map(m => m.networkId);
261
+ if (userNetworkIds.length === 0) {
262
+ prepLog.verbose('User has no network memberships - cannot find opportunities');
263
+ return {
264
+ userNetworks: [],
265
+ sourceProfile: null,
266
+ error: 'You need to join at least one network to find opportunities.',
267
+ };
268
+ }
269
+ const discoveryUserId = state.onBehalfOfUserId ?? state.userId;
270
+ const [intents, profile] = await Promise.all([
271
+ this.database.getActiveIntents(discoveryUserId),
272
+ this.database.getProfile(discoveryUserId),
273
+ ]);
274
+ const indexedIntents = intents.map((intent) => ({
275
+ intentId: intent.id,
276
+ payload: intent.payload,
277
+ summary: intent.summary ?? undefined,
278
+ indexes: [],
279
+ }));
280
+ const sourceProfile = profile
281
+ ? {
282
+ identity: profile.identity ?? undefined,
283
+ context: profile.context ?? undefined,
284
+ }
285
+ : null;
286
+ // Source premises are loaded after scope is resolved so premise discovery
287
+ // only uses premises assigned to the target network(s), and only up to
288
+ // DISCOVERY_SOURCE_PREMISE_LIMIT. Loading all premises here caused
289
+ // BACKEND-5: thousands of parallel vector searches for premise-rich users.
290
+ const sourcePremises = [];
291
+ const contextToIntentEnabled = process.env.DISCOVERY_CONTEXT_TO_INTENT !== '0';
292
+ const rawContexts = contextToIntentEnabled && typeof this.database.getUserContexts === 'function'
293
+ ? await this.database.getUserContexts(discoveryUserId)
294
+ : [];
295
+ const sourceContexts = rawContexts
296
+ // The global row (networkId: null) is excluded here — it is not in
297
+ // userNetworkIds — so context-to-intent discovery stays network-scoped.
298
+ .filter((c) => c.embedding && c.embedding.length > 0 && c.networkId !== null && userNetworkIds.includes(c.networkId))
299
+ .map((c) => ({
300
+ contextId: c.id,
301
+ networkId: c.networkId,
302
+ text: c.text,
303
+ embedding: c.embedding,
304
+ }));
305
+ return {
306
+ userNetworks: userNetworkIds,
307
+ indexedIntents,
308
+ sourceProfile,
309
+ sourcePremises,
310
+ sourceContexts,
311
+ trace: [{
312
+ node: "prep",
313
+ detail: `${userNetworkIds.length} network(s), ${intents.length} intent(s), premise discovery deferred, ${sourceContexts.length} context(s), ${profile ? 'profile loaded' : 'no profile'}`,
314
+ }],
315
+ };
316
+ }, { context: { userId: state.userId }, logOutput: true }).catch((error) => {
317
+ const errMsg = error instanceof Error ? error.message : String(error);
318
+ prepLog.error('Failed', { error });
319
+ return {
320
+ error: 'Failed to prepare opportunity search. Please try again.',
321
+ trace: [{
322
+ node: "prep_fatal",
323
+ detail: `Prep failed: ${errMsg}`,
324
+ data: { error: errMsg },
325
+ }],
326
+ };
327
+ })), (result) => {
328
+ const r = result;
329
+ if (r?.error)
330
+ return `error: ${r.error}`;
331
+ const indexes = r?.userNetworks;
332
+ const intents = r?.indexedIntents;
333
+ return indexes && intents ? `${indexes.length} index(es), ${intents.length} intent(s)` : undefined;
334
+ });
335
+ /**
336
+ * Node 1: Scope
337
+ * Determines which indexes to search within.
338
+ * If networkId provided: searches only that index.
339
+ * Otherwise: searches all user's indexes.
340
+ */
341
+ const scopeNode = withNodeTrace("opportunity-scope", async (state) => {
342
+ return timed("OpportunityGraph.scope", async () => {
343
+ scopeLog.verbose('Determining search scope', {
344
+ requestedIndexId: state.networkId,
345
+ userNetworksCount: state.userNetworks.length,
346
+ });
347
+ try {
348
+ let targetIndexIds;
349
+ if (state.networkId) {
350
+ // Validate user is member or owner of requested network
351
+ const isInScope = state.userNetworks.includes(state.networkId);
352
+ const isOwner = !isInScope && await this.database.isIndexOwner(state.networkId, state.userId);
353
+ if (!isInScope && !isOwner) {
354
+ scopeLog.warn('User not member of requested network', {
355
+ networkId: state.networkId,
356
+ });
357
+ return {
358
+ targetNetworks: [],
359
+ error: 'You are not a member of that network.',
360
+ };
361
+ }
362
+ targetIndexIds = [state.networkId];
363
+ }
364
+ else if (state.indexScope !== undefined) {
365
+ // Bounded scope (e.g. a network-scoped agent's reachable networks):
366
+ // intersect with the user's actual memberships so discovery never
367
+ // reaches networks outside the agent's bound scope. An explicit
368
+ // empty scope is authoritative and must fail closed.
369
+ const allowed = new Set(state.indexScope);
370
+ targetIndexIds = state.userNetworks.filter((n) => allowed.has(n));
371
+ scopeLog.verbose('Applied indexScope intersection', {
372
+ indexScopeCount: state.indexScope.length,
373
+ userNetworksCount: state.userNetworks.length,
374
+ targetCount: targetIndexIds.length,
375
+ });
376
+ }
377
+ else {
378
+ // Search all user's indexes
379
+ targetIndexIds = state.userNetworks;
380
+ }
381
+ if (state.triggerIntentId) {
382
+ // A trigger intent is an authoritative discovery boundary, not just
383
+ // ranking context. Recompute the intersection at the graph edge so
384
+ // direct chat/MCP callers cannot bypass FromIntentQueue admission.
385
+ const assignedNetworkIds = new Set(await this.database.getNetworkIdsForIntent(state.triggerIntentId));
386
+ const activeOwnerNetworkIds = new Set(state.userNetworks);
387
+ targetIndexIds = targetIndexIds.filter((networkId) => assignedNetworkIds.has(networkId) && activeOwnerNetworkIds.has(networkId));
388
+ scopeLog.verbose('Applied trigger-intent network intersection', {
389
+ triggerIntentId: state.triggerIntentId,
390
+ assignedCount: assignedNetworkIds.size,
391
+ targetCount: targetIndexIds.length,
392
+ });
393
+ }
394
+ // Fetch index details
395
+ const targetNetworks = await Promise.all(targetIndexIds.map(async (networkId) => {
396
+ const index = await this.database.getNetwork(networkId);
397
+ const memberCount = await this.database.getNetworkMemberCount(networkId);
398
+ return {
399
+ networkId,
400
+ title: index?.title ?? 'Unknown',
401
+ memberCount,
402
+ };
403
+ }));
404
+ scopeLog.verbose('Scope determined', {
405
+ targetIndexesCount: targetNetworks.length,
406
+ indexes: targetNetworks.map(i => i.title),
407
+ });
408
+ // ── Populate index relevancy scores for dedup tie-breaking ──
409
+ const indexRelevancyScores = {};
410
+ if (state.triggerIntentId) {
411
+ // Background path: look up persisted scores from intent_indexes
412
+ try {
413
+ const scores = await this.database.getIntentIndexScores(state.triggerIntentId);
414
+ for (const { networkId, relevancyScore } of scores) {
415
+ if (relevancyScore != null) {
416
+ indexRelevancyScores[networkId] = relevancyScore;
417
+ }
418
+ }
419
+ }
420
+ catch (err) {
421
+ scopeLog.warn('Failed to load intent index scores', { triggerIntentId: state.triggerIntentId, error: err });
422
+ }
423
+ }
424
+ else if (state.searchQuery?.trim()) {
425
+ // Chat path: score query against target indexes in parallel
426
+ try {
427
+ const indexer = new IntentIndexer();
428
+ const scopeAgentTimings = [];
429
+ const scorableIndexes = targetNetworks.filter(ti => ti.title !== 'Unknown');
430
+ const scoringPromises = scorableIndexes.map(async (ti) => {
431
+ const ctx = await this.database.getNetworkMemberContext(ti.networkId, state.userId);
432
+ if (!ctx?.indexPrompt?.trim() && !ctx?.memberPrompt?.trim()) {
433
+ return { networkId: ti.networkId, score: 1.0 };
434
+ }
435
+ const _indexerStart = Date.now();
436
+ const traceEmitter = requestContext.getStore()?.traceEmitter;
437
+ traceEmitter?.({ type: "agent_start", name: "intent-networker" });
438
+ let result = null;
439
+ try {
440
+ result = await indexer.invoke(state.searchQuery, ctx?.indexPrompt ?? null, ctx?.memberPrompt ?? null);
441
+ }
442
+ catch {
443
+ return { networkId: ti.networkId, score: 1.0 };
444
+ }
445
+ finally {
446
+ const _indexerDuration = Date.now() - _indexerStart;
447
+ traceEmitter?.({ type: "agent_end", name: "intent-networker", durationMs: _indexerDuration, summary: `Scored index ${ti.networkId}` });
448
+ scopeAgentTimings.push({ name: 'intent.indexer', durationMs: _indexerDuration });
449
+ }
450
+ if (!result)
451
+ return { networkId: ti.networkId, score: 1.0 };
452
+ const score = ctx?.indexPrompt && ctx?.memberPrompt
453
+ ? result.indexScore * 0.6 + result.memberScore * 0.4
454
+ : ctx?.indexPrompt ? result.indexScore : result.memberScore;
455
+ return { networkId: ti.networkId, score };
456
+ });
457
+ const results = await Promise.all(scoringPromises);
458
+ for (const { networkId, score } of results) {
459
+ indexRelevancyScores[networkId] = score;
460
+ }
461
+ // Accumulate indexer timings into graph state
462
+ if (scopeAgentTimings.length > 0) {
463
+ return {
464
+ targetNetworks,
465
+ indexRelevancyScores,
466
+ agentTimings: scopeAgentTimings,
467
+ trace: [{
468
+ node: "scope",
469
+ detail: `Searching ${targetNetworks.length} index(es): ${targetNetworks.map(i => `${i.title} (${i.memberCount})`).join(', ')}`,
470
+ data: { totalMembers: targetNetworks.reduce((sum, i) => sum + i.memberCount, 0) },
471
+ }],
472
+ };
473
+ }
474
+ }
475
+ catch (err) {
476
+ scopeLog.warn('Failed to score query against indexes', { error: err });
477
+ }
478
+ }
479
+ const totalMembers = targetNetworks.reduce((sum, i) => sum + i.memberCount, 0);
480
+ return {
481
+ targetNetworks,
482
+ indexRelevancyScores,
483
+ trace: [{
484
+ node: "scope",
485
+ detail: `Searching ${targetNetworks.length} index(es): ${targetNetworks.map(i => `${i.title} (${i.memberCount})`).join(', ')}`,
486
+ data: { totalMembers },
487
+ }],
488
+ };
489
+ }
490
+ catch (error) {
491
+ const errMsg = error instanceof Error ? error.message : String(error);
492
+ scopeLog.error('Failed', { error });
493
+ return {
494
+ targetNetworks: [],
495
+ error: 'Failed to determine search scope.',
496
+ trace: [{
497
+ node: "scope_fatal",
498
+ detail: `Scope failed: ${errMsg}`,
499
+ data: { error: errMsg },
500
+ }],
501
+ };
502
+ }
503
+ });
504
+ }, (result) => {
505
+ const r = result;
506
+ if (r?.error)
507
+ return `error: ${r.error}`;
508
+ const indexes = r?.targetNetworks;
509
+ return indexes ? `${indexes.length} index(es) in scope` : undefined;
510
+ });
511
+ /**
512
+ * Node 2: Resolve
513
+ * Resolves trigger intent from triggerIntentId or searchQuery vs indexedIntents;
514
+ * sets discoverySource, resolvedTriggerIntentId, resolvedIntentInIndex for routing (path A/B/C).
515
+ */
516
+ const resolveNode = withNodeTrace("opportunity-resolve", async (state) => {
517
+ return timed("OpportunityGraph.resolve", async () => {
518
+ resolveLog.verbose('Resolving intent and network membership', {
519
+ triggerIntentId: state.triggerIntentId,
520
+ hasSearchQuery: !!state.searchQuery,
521
+ indexedIntentsCount: state.indexedIntents.length,
522
+ });
523
+ const targetIndexIds = state.targetNetworks.map((t) => t.networkId);
524
+ try {
525
+ let resolvedIntentId;
526
+ if (state.triggerIntentId) {
527
+ const isOwnedActiveIntent = state.indexedIntents.some((intent) => intent.intentId === state.triggerIntentId);
528
+ if (!isOwnedActiveIntent) {
529
+ resolveLog.warn('Trigger intent is not an active intent owned by the discovery user', {
530
+ triggerIntentId: state.triggerIntentId,
531
+ userId: state.userId,
532
+ });
533
+ return {
534
+ resolvedTriggerIntentId: undefined,
535
+ resolvedIntentInIndex: false,
536
+ discoverySource: 'context',
537
+ error: 'Trigger intent is not available for discovery.',
538
+ };
539
+ }
540
+ const inNetwork = await this.database.getNetworkIdsForIntent(state.triggerIntentId);
541
+ const inTarget = inNetwork.some((id) => targetIndexIds.includes(id));
542
+ resolvedIntentId = state.triggerIntentId;
543
+ const resolvedIntentInIndex = inTarget;
544
+ const discoverySource = resolvedIntentInIndex ? 'intent' : 'context';
545
+ return {
546
+ resolvedTriggerIntentId: resolvedIntentId,
547
+ resolvedIntentInIndex,
548
+ discoverySource,
549
+ };
550
+ }
551
+ if (state.searchQuery?.trim() && state.indexedIntents.length > 0) {
552
+ const q = state.searchQuery.trim().toLowerCase();
553
+ const matched = state.indexedIntents.find((i) => i.payload?.toLowerCase().includes(q));
554
+ if (matched) {
555
+ resolvedIntentId = matched.intentId;
556
+ const inNetwork = await this.database.getNetworkIdsForIntent(matched.intentId);
557
+ const resolvedIntentInIndex = inNetwork.some((id) => targetIndexIds.includes(id));
558
+ const discoverySource = resolvedIntentInIndex ? 'intent' : 'context';
559
+ return {
560
+ resolvedTriggerIntentId: resolvedIntentId,
561
+ resolvedIntentInIndex,
562
+ discoverySource,
563
+ };
564
+ }
565
+ resolveLog.warn('No intent matched search query; leaving resolvedIntentId unset', {
566
+ searchQuery: state.searchQuery,
567
+ indexedIntentsCount: state.indexedIntents.length,
568
+ });
569
+ }
570
+ return {
571
+ resolvedTriggerIntentId: undefined,
572
+ resolvedIntentInIndex: false,
573
+ discoverySource: 'context',
574
+ };
575
+ }
576
+ catch (err) {
577
+ const errMsg = err instanceof Error ? err.message : String(err);
578
+ resolveLog.error('Failed', {
579
+ triggerIntentId: state.triggerIntentId,
580
+ searchQuery: state.searchQuery,
581
+ error: err,
582
+ });
583
+ return {
584
+ resolvedTriggerIntentId: undefined,
585
+ resolvedIntentInIndex: false,
586
+ discoverySource: 'context',
587
+ error: errMsg || 'Resolve failed',
588
+ trace: [{
589
+ node: "resolve_fatal",
590
+ detail: `Resolve failed: ${errMsg}`,
591
+ data: { error: errMsg },
592
+ }],
593
+ };
594
+ }
595
+ });
596
+ }, (result) => {
597
+ const r = result;
598
+ if (r?.error)
599
+ return `error: ${r.error}`;
600
+ return r?.discoverySource ? `source: ${r.discoverySource}` : undefined;
601
+ });
602
+ /**
603
+ * Node 3: Discovery
604
+ * Generates HyDE embeddings and performs semantic search.
605
+ */
606
+ const discoveryNode = withNodeTrace("opportunity-discovery", async (state) => {
607
+ const self = this;
608
+ return timed("OpportunityGraph.discovery", async () => {
609
+ const startTime = Date.now();
610
+ const discoveryUserId = state.onBehalfOfUserId ?? state.userId;
611
+ /** Filter candidates to targetUserId when set (direct-connection mode). */
612
+ const filterByTarget = (candidates) => {
613
+ if (!state.targetUserId)
614
+ return candidates;
615
+ const filtered = candidates.filter(c => c.candidateUserId === state.targetUserId);
616
+ discoveryLog.verbose('targetUserId filter applied', {
617
+ targetUserId: state.targetUserId,
618
+ before: candidates.length,
619
+ after: filtered.length,
620
+ });
621
+ return filtered;
622
+ };
623
+ // Shared variable to capture lens input data from runQueryHydeDiscovery or intent path
624
+ let discoveryLensInput;
625
+ // Shared variable to capture HyDE output (lenses + documents) for trace entries
626
+ let discoveryHydeOutput;
627
+ discoveryLog.verbose('Starting semantic search', {
628
+ targetIndexesCount: state.targetNetworks.length,
629
+ discoverySource: state.discoverySource,
630
+ searchQueryPreview: state.searchQuery?.trim().slice(0, 60) ?? '(none)',
631
+ });
632
+ try {
633
+ if (state.targetNetworks.length === 0) {
634
+ discoveryLog.warn('No target indexes for search');
635
+ return { candidates: [] };
636
+ }
637
+ // ── Direct-connection fast path ──
638
+ // When targetUserId is set (user @-mentioned someone), bypass vector search
639
+ // and construct candidates directly from shared networks.
640
+ if (state.targetUserId) {
641
+ if (state.targetUserId === discoveryUserId) {
642
+ discoveryLog.warn('Direct-connection target matches discoverer; skipping self-match', {
643
+ targetUserId: state.targetUserId,
644
+ });
645
+ return {
646
+ candidates: [],
647
+ trace: [{
648
+ node: "discovery",
649
+ detail: "Direct connection skipped: target user is discoverer",
650
+ data: { targetUserId: state.targetUserId },
651
+ }],
652
+ };
653
+ }
654
+ discoveryLog.verbose('Direct-connection mode — bypassing vector search', {
655
+ targetUserId: state.targetUserId,
656
+ });
657
+ const targetMemberships = await this.database.getNetworkMemberships(state.targetUserId);
658
+ const targetUserIndexIds = targetMemberships.map(m => m.networkId);
659
+ const sharedIndexIds = state.targetNetworks
660
+ .filter(ti => targetUserIndexIds.includes(ti.networkId))
661
+ .map(ti => ti.networkId);
662
+ if (sharedIndexIds.length === 0) {
663
+ discoveryLog.warn('Target user shares no indexes with discoverer', {
664
+ targetUserId: state.targetUserId,
665
+ discovererIndexes: state.targetNetworks.map(ti => ti.networkId),
666
+ });
667
+ return {
668
+ candidates: [],
669
+ trace: [{
670
+ node: "discovery",
671
+ detail: `Direct connection: target user shares no indexes`,
672
+ data: { targetUserId: state.targetUserId },
673
+ }],
674
+ };
675
+ }
676
+ // Fetch target user's active intents to build intent-level candidates
677
+ const targetIntents = await this.database.getActiveIntents(state.targetUserId);
678
+ const directCandidates = [];
679
+ if (targetIntents.length > 0) {
680
+ // Build one candidate per intent per shared network it belongs to
681
+ for (const intent of targetIntents) {
682
+ const intentNetworkIds = await this.database.getNetworkIdsForIntent(intent.id);
683
+ const overlapping = sharedIndexIds.filter(id => intentNetworkIds.includes(id));
684
+ for (const networkId of overlapping) {
685
+ directCandidates.push(withCandidateEvidence({
686
+ candidateUserId: state.targetUserId,
687
+ candidateIntentId: intent.id,
688
+ networkId,
689
+ similarity: 1.0,
690
+ lens: 'explicit_mention',
691
+ candidatePayload: intent.payload,
692
+ candidateSummary: intent.summary ?? undefined,
693
+ discoverySource: 'query',
694
+ }));
695
+ }
696
+ }
697
+ }
698
+ // Always add a profile-level candidate (so evaluation runs even without intents)
699
+ if (directCandidates.length === 0) {
700
+ directCandidates.push(withCandidateEvidence({
701
+ candidateUserId: state.targetUserId,
702
+ networkId: sharedIndexIds[0],
703
+ similarity: 1.0,
704
+ lens: 'explicit_mention',
705
+ candidatePayload: '',
706
+ candidateSummary: undefined,
707
+ discoverySource: 'query',
708
+ }));
709
+ }
710
+ discoveryLog.verbose('Direct candidates constructed', {
711
+ count: directCandidates.length,
712
+ sharedIndexes: sharedIndexIds.length,
713
+ targetIntents: targetIntents.length,
714
+ });
715
+ return {
716
+ candidates: directCandidates,
717
+ trace: [{
718
+ node: "discovery",
719
+ detail: `Direct connection → ${directCandidates.length} candidate(s) from ${sharedIndexIds.length} shared network(es)`,
720
+ data: {
721
+ targetUserId: state.targetUserId,
722
+ candidateCount: directCandidates.length,
723
+ sharedIndexes: sharedIndexIds.length,
724
+ durationMs: Date.now() - startTime,
725
+ },
726
+ }],
727
+ };
728
+ }
729
+ // Search limits - fixed values for candidate retrieval
730
+ // (The options.limit controls final output, not search pool)
731
+ const limitPerStrategy = 30;
732
+ const perIndexLimit = 80;
733
+ // Similarity threshold for recall (0.30 = 30% similarity)
734
+ const minScore = 0.3;
735
+ if (state.discoverySource === 'context') {
736
+ // Context discovery: HyDE (when search query exists) + premise-to-premise.
737
+ if (state.searchQuery?.trim()) {
738
+ discoveryLog.verbose('Context source with searchQuery → running query HyDE + premise paths', {
739
+ searchQuery: state.searchQuery.trim().substring(0, 80),
740
+ });
741
+ const queryCandidates = await runQueryHydeDiscovery();
742
+ discoveryLog.verbose('Query HyDE path complete', { candidatesFound: queryCandidates.length });
743
+ // Build trace entries for this path
744
+ const traceEntries = [];
745
+ // Lens input trace (captured from runQueryHydeDiscovery)
746
+ if (discoveryLensInput) {
747
+ traceEntries.push({
748
+ node: "lens_input",
749
+ detail: "Profile context for lens inference",
750
+ data: discoveryLensInput,
751
+ });
752
+ }
753
+ // Lens output and HyDE document traces (captured from runQueryHydeDiscovery)
754
+ if (discoveryHydeOutput) {
755
+ if (discoveryHydeOutput.lenses.length > 0) {
756
+ traceEntries.push({
757
+ node: "lens_output",
758
+ detail: `Inferred ${discoveryHydeOutput.lenses.length} lens(es): ${discoveryHydeOutput.lenses.map(l => l.label).join(', ')}`,
759
+ data: { lenses: discoveryHydeOutput.lenses, model: getModelName("lensInferrer") },
760
+ });
761
+ }
762
+ for (const [lens, doc] of Object.entries(discoveryHydeOutput.hydeDocuments)) {
763
+ if (doc?.hydeText) {
764
+ traceEntries.push({
765
+ node: "hyde_query",
766
+ detail: `[${lens}] "${doc.hydeText.slice(0, 120)}${doc.hydeText.length > 120 ? '...' : ''}"`,
767
+ data: { lens, hydeTextPreview: doc.hydeText.slice(0, 300) + (doc.hydeText.length > 300 ? '...' : '') },
768
+ });
769
+ }
770
+ }
771
+ }
772
+ // Compute per-lens stats from deduped candidates
773
+ const lensStats = {};
774
+ for (const c of queryCandidates) {
775
+ const s = c.lens || 'unknown';
776
+ if (!lensStats[s])
777
+ lensStats[s] = { count: 0, avgSimilarity: 0 };
778
+ lensStats[s].count++;
779
+ lensStats[s].avgSimilarity += c.similarity;
780
+ }
781
+ for (const s of Object.values(lensStats)) {
782
+ s.avgSimilarity = s.count > 0 ? Math.round((s.avgSimilarity / s.count) * 1000) / 1000 : 0;
783
+ }
784
+ traceEntries.push({
785
+ node: "discovery",
786
+ detail: `HyDE search → ${queryCandidates.length} candidate(s) from query path`,
787
+ data: {
788
+ candidateCount: queryCandidates.length,
789
+ byLens: lensStats,
790
+ searchQuery: state.searchQuery?.trim().slice(0, 80),
791
+ durationMs: Date.now() - startTime,
792
+ model: getModelName("hydeGenerator"),
793
+ },
794
+ });
795
+ const [premiseCands, contextCands] = await Promise.all([
796
+ runPremiseDiscovery(),
797
+ runContextToIntentDiscovery(),
798
+ ]);
799
+ const withPremisesAndContext = mergeStrategyCandidates(queryCandidates, premiseCands, contextCands);
800
+ if (premiseCands.length > 0) {
801
+ traceEntries.push({ node: "strategy", detail: `premise-to-premise → ${premiseCands.length} candidate(s)` });
802
+ }
803
+ if (contextCands.length > 0) {
804
+ traceEntries.push({ node: "strategy", detail: `context-to-intent → ${contextCands.length} candidate(s)` });
805
+ }
806
+ return { candidates: filterByTarget(withPremisesAndContext), trace: traceEntries };
807
+ }
808
+ // No search query — premise-to-premise + context-to-intent discovery
809
+ const [premiseCands, contextCands] = await Promise.all([
810
+ runPremiseDiscovery(),
811
+ runContextToIntentDiscovery(),
812
+ ]);
813
+ if (premiseCands.length > 0 || contextCands.length > 0) {
814
+ const merged = mergeStrategyCandidates(premiseCands, contextCands);
815
+ const traceEntries = [];
816
+ if (premiseCands.length > 0) {
817
+ traceEntries.push({ node: "strategy", detail: `premise-to-premise → ${premiseCands.length} candidate(s)` });
818
+ }
819
+ if (contextCands.length > 0) {
820
+ traceEntries.push({ node: "strategy", detail: `context-to-intent → ${contextCands.length} candidate(s)` });
821
+ }
822
+ traceEntries.push({
823
+ node: "discovery",
824
+ 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`,
825
+ });
826
+ return { candidates: filterByTarget(merged), trace: traceEntries };
827
+ }
828
+ return { candidates: [] };
829
+ }
830
+ async function runQueryHydeDiscovery() {
831
+ const searchText = state.searchQuery?.trim() ?? '';
832
+ if (!searchText)
833
+ return [];
834
+ discoveryLog.verbose('runQueryHydeDiscovery start', { searchText: searchText.slice(0, 80) });
835
+ const discovererContext = buildDiscovererContext(state.sourceProfile, state.indexedIntents);
836
+ discoveryLensInput = {
837
+ profileContext: discovererContext,
838
+ model: getModelName("lensInferrer"),
839
+ };
840
+ const hydeResult = await self.hydeGenerator.invoke({
841
+ sourceType: 'query',
842
+ sourceText: searchText,
843
+ forceRegenerate: false,
844
+ profileContext: discovererContext,
845
+ });
846
+ const hydeEmbeddings = hydeResult.hydeEmbeddings;
847
+ const lenses = hydeResult.lenses ?? [];
848
+ discoveryHydeOutput = {
849
+ lenses: lenses,
850
+ hydeDocuments: (hydeResult.hydeDocuments ?? {}),
851
+ };
852
+ const embeddingKeys = hydeEmbeddings ? Object.keys(hydeEmbeddings) : [];
853
+ discoveryLog.verbose('HyDE generator result', {
854
+ lensCount: embeddingKeys.length,
855
+ lenses: embeddingKeys,
856
+ });
857
+ if (!hydeEmbeddings || Object.keys(hydeEmbeddings).length === 0)
858
+ return [];
859
+ const lensMap = new Map(lenses.map(l => [l.label, l]));
860
+ const lensEmbeddings = [];
861
+ for (const [label, emb] of Object.entries(hydeEmbeddings)) {
862
+ if (emb?.length) {
863
+ const lens = lensMap.get(label);
864
+ lensEmbeddings.push({ lens: label, corpus: lens?.corpus ?? 'profiles', embedding: emb });
865
+ }
866
+ }
867
+ const all = [];
868
+ await Promise.all(state.targetNetworks.map(async (targetIndex) => {
869
+ const results = await self.embedder.searchWithHydeEmbeddings(lensEmbeddings, {
870
+ indexScope: [targetIndex.networkId],
871
+ excludeUserId: discoveryUserId,
872
+ limitPerStrategy,
873
+ limit: perIndexLimit,
874
+ minScore,
875
+ });
876
+ for (const r of results.filter((x) => x.type === 'intent')) {
877
+ all.push(withCandidateEvidence({
878
+ candidateUserId: r.userId,
879
+ candidateIntentId: r.id,
880
+ networkId: targetIndex.networkId,
881
+ similarity: r.score,
882
+ lens: r.matchedVia,
883
+ candidatePayload: '',
884
+ candidateSummary: undefined,
885
+ discoverySource: 'query',
886
+ }));
887
+ }
888
+ for (const r of results.filter((x) => x.type === 'premise')) {
889
+ all.push(withCandidateEvidence({
890
+ candidateUserId: r.userId,
891
+ candidatePremiseId: r.id,
892
+ networkId: targetIndex.networkId,
893
+ similarity: r.score,
894
+ lens: r.matchedVia,
895
+ candidatePayload: '',
896
+ candidateSummary: undefined,
897
+ discoverySource: 'query',
898
+ }));
899
+ }
900
+ }));
901
+ const intentCount = all.filter((c) => c.candidateIntentId).length;
902
+ const premiseCount = all.filter((c) => c.candidatePremiseId).length;
903
+ discoveryLog.verbose('searchWithHydeEmbeddings raw results', {
904
+ total: all.length,
905
+ fromIntent: intentCount,
906
+ fromPremise: premiseCount,
907
+ });
908
+ const byKey = new Map();
909
+ for (const c of all) {
910
+ // Dedup by candidateUserId + entity (intent or premise), NOT by indexId.
911
+ // Including indexId caused the same user to appear once per index they belong to.
912
+ const entityKey = c.candidateIntentId ? `intent:${c.candidateIntentId}` : `premise:${c.candidatePremiseId}`;
913
+ const key = `${c.candidateUserId}:${entityKey}`;
914
+ if (!byKey.has(key) || c.similarity > (byKey.get(key)?.similarity ?? 0)) {
915
+ byKey.set(key, c);
916
+ }
917
+ }
918
+ return Array.from(byKey.values());
919
+ }
920
+ /**
921
+ * Premise-to-premise discovery (path D).
922
+ * Searches for other users' premises similar to the discoverer's premises,
923
+ * scoped to target networks. Additive — merges into existing candidates.
924
+ */
925
+ async function runPremiseDiscovery() {
926
+ const targetNetworkIds = state.targetNetworks.map(t => t.networkId);
927
+ if (targetNetworkIds.length === 0)
928
+ return [];
929
+ const sourceLimit = getSourcePremiseDiscoveryLimit();
930
+ if (sourceLimit === 0) {
931
+ discoveryLog.verbose('runPremiseDiscovery disabled by DISCOVERY_SOURCE_PREMISE_LIMIT=0');
932
+ return [];
933
+ }
934
+ const sourcePremisesFromDb = self.database.getPremisesForUserInNetworks
935
+ ? await self.database.getPremisesForUserInNetworks(discoveryUserId, targetNetworkIds, 'ACTIVE', sourceLimit)
936
+ : await self.database.getPremisesForUser(discoveryUserId, 'ACTIVE');
937
+ const sourcePremises = (sourcePremisesFromDb.length > 0
938
+ ? sourcePremisesFromDb
939
+ .filter(p => Array.isArray(p.embedding) && p.embedding.length > 0)
940
+ .slice(0, sourceLimit)
941
+ .map(p => ({ premiseId: p.id, embedding: p.embedding }))
942
+ : (state.sourcePremises ?? []).slice(0, sourceLimit));
943
+ if (sourcePremises.length === 0)
944
+ return [];
945
+ discoveryLog.verbose('runPremiseDiscovery start', {
946
+ premiseCount: sourcePremises.length,
947
+ sourceLimit,
948
+ targetNetworks: targetNetworkIds.length,
949
+ batched: !!self.database.searchPremisesBySimilarityBatch,
950
+ });
951
+ const rawResults = self.database.searchPremisesBySimilarityBatch
952
+ ? await self.database.searchPremisesBySimilarityBatch({
953
+ sources: sourcePremises,
954
+ networkIds: targetNetworkIds,
955
+ excludeUserId: discoveryUserId,
956
+ limitPerSource: PREMISE_MATCH_LIMIT_PER_SOURCE,
957
+ })
958
+ : (await Promise.all(sourcePremises.map(async (sp) => {
959
+ const results = await self.database.searchPremisesBySimilarity({
960
+ embedding: sp.embedding,
961
+ networkIds: targetNetworkIds,
962
+ excludeUserId: discoveryUserId,
963
+ limit: PREMISE_MATCH_LIMIT_PER_SOURCE,
964
+ });
965
+ return results.map((r) => ({ ...r, sourcePremiseId: sp.premiseId }));
966
+ }))).flat();
967
+ const premiseCandidates = [];
968
+ for (const r of rawResults) {
969
+ premiseCandidates.push(withCandidateEvidence({
970
+ candidateUserId: r.userId,
971
+ sourcePremiseId: r.sourcePremiseId,
972
+ candidatePremiseId: r.premiseId,
973
+ networkId: r.networkId,
974
+ similarity: typeof r.similarity === 'number' ? r.similarity : parseFloat(String(r.similarity)),
975
+ lens: 'premise_match',
976
+ candidatePayload: r.assertionText ?? '',
977
+ discoverySource: 'premise-similarity',
978
+ }));
979
+ }
980
+ // Dedup by userId + premiseId + networkId (a premise can appear in multiple networks)
981
+ const byKey = new Map();
982
+ for (const c of premiseCandidates) {
983
+ const key = `${c.candidateUserId}:${c.candidatePremiseId ?? 'none'}:${c.networkId}`;
984
+ if (!byKey.has(key) || c.similarity > (byKey.get(key)?.similarity ?? 0)) {
985
+ byKey.set(key, c);
986
+ }
987
+ }
988
+ const deduped = Array.from(byKey.values());
989
+ discoveryLog.verbose('runPremiseDiscovery complete', {
990
+ sourcePremiseCount: sourcePremises.length,
991
+ rawCount: premiseCandidates.length,
992
+ dedupedCount: deduped.length,
993
+ });
994
+ return deduped;
995
+ }
996
+ /**
997
+ * Context-to-intent discovery: searches intents using context HyDE embeddings.
998
+ * When HyDE documents exist for a context, uses optimised hypothetical-document
999
+ * embeddings via searchWithHydeEmbeddings. Falls back to raw context embedding
1000
+ * via searchIntentsByContextEmbedding when no HyDE docs are available.
1001
+ */
1002
+ async function runContextToIntentDiscovery() {
1003
+ if (!state.sourceContexts?.length)
1004
+ return [];
1005
+ const contextToIntentEnabled = process.env.DISCOVERY_CONTEXT_TO_INTENT !== '0';
1006
+ if (!contextToIntentEnabled)
1007
+ return [];
1008
+ const targetNetworkIds = state.targetNetworks.map(t => t.networkId);
1009
+ if (targetNetworkIds.length === 0)
1010
+ return [];
1011
+ discoveryLog.verbose('runContextToIntentDiscovery start', {
1012
+ contextCount: state.sourceContexts.length,
1013
+ targetNetworks: targetNetworkIds.length,
1014
+ });
1015
+ const contextCandidates = [];
1016
+ for (const ctx of state.sourceContexts.filter(c => targetNetworkIds.includes(c.networkId))) {
1017
+ // Attempt HyDE-enhanced search first
1018
+ const persistedHydeDocs = await self.database.getHydeDocumentsForSource('context', ctx.contextId);
1019
+ const hydeDocs = selectHydeDocumentsForGeneration(persistedHydeDocs, getHydeGenerationMode(), ctx.text);
1020
+ const lensEmbeddings = hydeDocs
1021
+ .filter(d => d.hydeEmbedding?.length > 0)
1022
+ .map(d => ({
1023
+ lens: d.strategy,
1024
+ corpus: (d.targetCorpus === 'intents' ? 'intents' : d.targetCorpus === 'premises' ? 'premises' : 'intents'),
1025
+ embedding: d.hydeEmbedding,
1026
+ }));
1027
+ if (lensEmbeddings.length > 0) {
1028
+ // HyDE-enhanced search: same path as query HyDE, scoped to this context's network
1029
+ const results = await self.embedder.searchWithHydeEmbeddings(lensEmbeddings, {
1030
+ indexScope: [ctx.networkId],
1031
+ excludeUserId: discoveryUserId,
1032
+ limitPerStrategy: limitPerStrategy,
1033
+ limit: 20,
1034
+ minScore,
1035
+ });
1036
+ for (const r of results.filter(r => r.type === 'intent')) {
1037
+ contextCandidates.push(withCandidateEvidence({
1038
+ candidateUserId: r.userId,
1039
+ candidateIntentId: r.id,
1040
+ sourceContextId: ctx.contextId,
1041
+ networkId: ctx.networkId,
1042
+ similarity: r.score,
1043
+ lens: r.matchedVia,
1044
+ candidatePayload: '',
1045
+ candidateSummary: undefined,
1046
+ discoverySource: 'context-to-intent',
1047
+ }));
1048
+ }
1049
+ }
1050
+ else {
1051
+ // Fallback: raw context embedding search (no HyDE docs yet)
1052
+ const results = await self.database.searchIntentsByContextEmbedding({
1053
+ embedding: ctx.embedding,
1054
+ networkIds: [ctx.networkId],
1055
+ excludeUserId: discoveryUserId,
1056
+ limit: 20,
1057
+ minScore: minScore,
1058
+ });
1059
+ for (const r of results) {
1060
+ contextCandidates.push(withCandidateEvidence({
1061
+ candidateUserId: r.userId,
1062
+ candidateIntentId: r.intentId,
1063
+ sourceContextId: ctx.contextId,
1064
+ networkId: r.networkId,
1065
+ similarity: typeof r.similarity === 'number' ? r.similarity : parseFloat(String(r.similarity)),
1066
+ lens: 'context_match',
1067
+ candidatePayload: r.payload ?? '',
1068
+ candidateSummary: r.summary ?? undefined,
1069
+ discoverySource: 'context-to-intent',
1070
+ }));
1071
+ }
1072
+ }
1073
+ }
1074
+ const byKey = new Map();
1075
+ for (const c of contextCandidates) {
1076
+ const key = `${c.candidateUserId}:${c.candidateIntentId ?? 'none'}:${c.networkId}`;
1077
+ if (!byKey.has(key) || c.similarity > (byKey.get(key)?.similarity ?? 0)) {
1078
+ byKey.set(key, c);
1079
+ }
1080
+ }
1081
+ const deduped = Array.from(byKey.values());
1082
+ discoveryLog.verbose('runContextToIntentDiscovery complete', {
1083
+ rawCount: contextCandidates.length,
1084
+ dedupedCount: deduped.length,
1085
+ });
1086
+ return deduped;
1087
+ }
1088
+ /**
1089
+ * Merge candidates from multiple strategies. Deduplicates by userId + networkId + entityId,
1090
+ * keeps the highest similarity, tracks which strategies found each candidate,
1091
+ * and applies a multi-strategy boost (+0.05 per additional strategy, boost capped at 0.15,
1092
+ * final similarity capped at 1.0).
1093
+ */
1094
+ function mergeStrategyCandidates(...groups) {
1095
+ const merged = new Map();
1096
+ for (const group of groups) {
1097
+ for (const c of group) {
1098
+ const entityId = c.candidateIntentId ?? c.candidatePremiseId ?? 'none';
1099
+ const key = `${c.candidateUserId}:${c.networkId}:${entityId}`;
1100
+ const existing = merged.get(key);
1101
+ if (!existing) {
1102
+ merged.set(key, { ...c, _strategies: new Set([c.discoverySource ?? 'unknown']) });
1103
+ }
1104
+ else {
1105
+ existing._strategies.add(c.discoverySource ?? 'unknown');
1106
+ const mergedEvidence = mergeOpportunityEvidence(existing.evidence, c.evidence);
1107
+ if (c.similarity > existing.similarity) {
1108
+ Object.assign(existing, { ...c, evidence: mergedEvidence });
1109
+ }
1110
+ else {
1111
+ existing.evidence = mergedEvidence;
1112
+ }
1113
+ }
1114
+ }
1115
+ }
1116
+ return Array.from(merged.values()).map(({ _strategies, ...c }) => {
1117
+ const matchedStrategies = Array.from(_strategies);
1118
+ const boost = Math.min((_strategies.size - 1) * 0.05, 0.15);
1119
+ return {
1120
+ ...c,
1121
+ similarity: Math.min(c.similarity + boost, 1.0),
1122
+ matchedStrategies,
1123
+ evidence: withMatchedStrategies(mergeOpportunityEvidence(c.evidence), matchedStrategies),
1124
+ };
1125
+ });
1126
+ }
1127
+ const resolvedIntent = state.resolvedTriggerIntentId
1128
+ ? state.indexedIntents.find((i) => i.intentId === state.resolvedTriggerIntentId)
1129
+ : state.indexedIntents[0];
1130
+ const searchText = state.searchQuery ?? resolvedIntent?.payload ?? '';
1131
+ if (!searchText) {
1132
+ discoveryLog.warn('No search text available for intent path');
1133
+ const [premiseCands, contextCands] = await Promise.all([
1134
+ runPremiseDiscovery(),
1135
+ runContextToIntentDiscovery(),
1136
+ ]);
1137
+ const merged = mergeStrategyCandidates(premiseCands, contextCands);
1138
+ if (merged.length > 0) {
1139
+ return {
1140
+ candidates: filterByTarget(merged),
1141
+ trace: [{ node: "discovery", detail: `No search text; premise → ${premiseCands.length}, context → ${contextCands.length}, merged → ${merged.length} candidate(s)` }],
1142
+ };
1143
+ }
1144
+ return { candidates: [] };
1145
+ }
1146
+ const discovererContext = buildDiscovererContext(state.sourceProfile, state.indexedIntents);
1147
+ discoveryLensInput = {
1148
+ profileContext: discovererContext,
1149
+ model: getModelName("lensInferrer"),
1150
+ };
1151
+ const hydeResult = await this.hydeGenerator.invoke({
1152
+ sourceType: 'query',
1153
+ sourceText: searchText,
1154
+ forceRegenerate: false,
1155
+ profileContext: discovererContext,
1156
+ });
1157
+ const hydeEmbeddings = hydeResult.hydeEmbeddings;
1158
+ const lenses = hydeResult.lenses ?? [];
1159
+ if (!hydeEmbeddings || Object.keys(hydeEmbeddings).length === 0) {
1160
+ const [premiseCands, contextCands] = await Promise.all([
1161
+ runPremiseDiscovery(),
1162
+ runContextToIntentDiscovery(),
1163
+ ]);
1164
+ const merged = mergeStrategyCandidates(premiseCands, contextCands);
1165
+ if (merged.length > 0) {
1166
+ return {
1167
+ hydeEmbeddings: {},
1168
+ candidates: filterByTarget(merged),
1169
+ trace: [{ node: "discovery", detail: `No HyDE embeddings; premise → ${premiseCands.length}, context → ${contextCands.length}, merged → ${merged.length} candidate(s)` }],
1170
+ };
1171
+ }
1172
+ return { hydeEmbeddings: {}, candidates: [] };
1173
+ }
1174
+ const lensMap = new Map(lenses.map(l => [l.label, l]));
1175
+ const lensEmbeddings = [];
1176
+ for (const [label, emb] of Object.entries(hydeEmbeddings)) {
1177
+ if (emb?.length) {
1178
+ const lens = lensMap.get(label);
1179
+ lensEmbeddings.push({ lens: label, corpus: lens?.corpus ?? 'profiles', embedding: emb });
1180
+ }
1181
+ }
1182
+ const allCandidates = [];
1183
+ await Promise.all(state.targetNetworks.map(async (targetIndex) => {
1184
+ const results = await this.embedder.searchWithHydeEmbeddings(lensEmbeddings, {
1185
+ indexScope: [targetIndex.networkId],
1186
+ excludeUserId: discoveryUserId,
1187
+ limitPerStrategy,
1188
+ limit: perIndexLimit,
1189
+ minScore,
1190
+ });
1191
+ for (const result of results.filter((r) => r.type === 'intent')) {
1192
+ allCandidates.push(withCandidateEvidence({
1193
+ candidateUserId: result.userId,
1194
+ candidateIntentId: result.id,
1195
+ networkId: targetIndex.networkId,
1196
+ similarity: result.score,
1197
+ lens: result.matchedVia,
1198
+ candidatePayload: '',
1199
+ candidateSummary: undefined,
1200
+ discoverySource: 'query',
1201
+ }));
1202
+ }
1203
+ for (const result of results.filter((r) => r.type === 'premise')) {
1204
+ allCandidates.push(withCandidateEvidence({
1205
+ candidateUserId: result.userId,
1206
+ candidatePremiseId: result.id,
1207
+ networkId: targetIndex.networkId,
1208
+ similarity: result.score,
1209
+ lens: result.matchedVia,
1210
+ candidatePayload: '',
1211
+ candidateSummary: undefined,
1212
+ discoverySource: 'query',
1213
+ }));
1214
+ }
1215
+ }));
1216
+ const byUserAndIndex = new Map();
1217
+ for (const c of allCandidates) {
1218
+ const entityKey = c.candidateIntentId ? `intent:${c.candidateIntentId}` : `premise:${c.candidatePremiseId}`;
1219
+ const key = `${c.candidateUserId}:${c.networkId}:${entityKey}`;
1220
+ if (!byUserAndIndex.has(key) || c.similarity > (byUserAndIndex.get(key)?.similarity ?? 0)) {
1221
+ byUserAndIndex.set(key, c);
1222
+ }
1223
+ }
1224
+ const candidates = Array.from(byUserAndIndex.values());
1225
+ discoveryLog.verbose('Intent-path discovery complete', { candidatesFound: candidates.length });
1226
+ const usedLenses = Object.keys(hydeEmbeddings);
1227
+ // Build trace with individual candidate similarity scores
1228
+ const traceEntries = [];
1229
+ // Lens input trace
1230
+ if (discoveryLensInput) {
1231
+ traceEntries.push({
1232
+ node: "lens_input",
1233
+ detail: "Profile context for lens inference",
1234
+ data: discoveryLensInput,
1235
+ });
1236
+ }
1237
+ // Lens output trace
1238
+ if (lenses.length > 0) {
1239
+ traceEntries.push({
1240
+ node: "lens_output",
1241
+ detail: `Inferred ${lenses.length} lens(es): ${lenses.map(l => l.label).join(', ')}`,
1242
+ data: { lenses, model: getModelName("lensInferrer") },
1243
+ });
1244
+ }
1245
+ // Compute per-lens stats from deduped candidates
1246
+ const lensStats = {};
1247
+ for (const c of candidates) {
1248
+ const s = c.lens || 'unknown';
1249
+ if (!lensStats[s])
1250
+ lensStats[s] = { count: 0, avgSimilarity: 0 };
1251
+ lensStats[s].count++;
1252
+ lensStats[s].avgSimilarity += c.similarity;
1253
+ }
1254
+ for (const s of Object.values(lensStats)) {
1255
+ s.avgSimilarity = s.count > 0 ? Math.round((s.avgSimilarity / s.count) * 1000) / 1000 : 0;
1256
+ }
1257
+ traceEntries.push({
1258
+ node: "discovery",
1259
+ detail: `Query: "${searchText.slice(0, 50)}${searchText.length > 50 ? '...' : ''}" → ${candidates.length} candidate(s)`,
1260
+ data: {
1261
+ query: searchText.slice(0, 100),
1262
+ lenses: usedLenses,
1263
+ candidateCount: candidates.length,
1264
+ byLens: lensStats,
1265
+ durationMs: Date.now() - startTime,
1266
+ model: getModelName("hydeGenerator"),
1267
+ },
1268
+ });
1269
+ // Show the HyDE-generated hypothetical documents used for search
1270
+ const hydeDocuments = hydeResult.hydeDocuments;
1271
+ if (hydeDocuments) {
1272
+ for (const [lens, doc] of Object.entries(hydeDocuments)) {
1273
+ if (doc?.hydeText) {
1274
+ traceEntries.push({
1275
+ node: "hyde_query",
1276
+ detail: `[${lens}] "${doc.hydeText.slice(0, 120)}${doc.hydeText.length > 120 ? '...' : ''}"`,
1277
+ data: {
1278
+ lens,
1279
+ hydeTextPreview: doc.hydeText.slice(0, 160) + (doc.hydeText.length > 160 ? '...' : ''),
1280
+ },
1281
+ });
1282
+ }
1283
+ }
1284
+ }
1285
+ // Add top candidates with similarity scores
1286
+ const sortedCandidates = [...candidates].sort((a, b) => b.similarity - a.similarity).slice(0, 10);
1287
+ for (const c of sortedCandidates) {
1288
+ traceEntries.push({
1289
+ node: "match",
1290
+ detail: `Similarity ${Math.round(c.similarity * 100)}% via ${c.lens}`,
1291
+ data: {
1292
+ userId: c.candidateUserId,
1293
+ similarity: Math.round(c.similarity * 100),
1294
+ lens: c.lens,
1295
+ hasIntent: !!c.candidateIntentId,
1296
+ },
1297
+ });
1298
+ }
1299
+ const [premiseCands, contextCands] = await Promise.all([
1300
+ runPremiseDiscovery(),
1301
+ runContextToIntentDiscovery(),
1302
+ ]);
1303
+ const allStrategies = mergeStrategyCandidates(candidates, premiseCands, contextCands);
1304
+ if (premiseCands.length > 0 || contextCands.length > 0) {
1305
+ traceEntries.push({
1306
+ node: "discovery",
1307
+ detail: `+ Premise → ${premiseCands.length}, Context → ${contextCands.length}, merged to ${allStrategies.length} candidate(s)`,
1308
+ });
1309
+ }
1310
+ return {
1311
+ hydeEmbeddings: hydeEmbeddings,
1312
+ candidates: filterByTarget(allStrategies),
1313
+ trace: traceEntries,
1314
+ };
1315
+ }
1316
+ catch (error) {
1317
+ const errMsg = error instanceof Error ? error.message : String(error);
1318
+ discoveryLog.error('Failed', { error });
1319
+ return {
1320
+ candidates: [],
1321
+ error: 'Failed to search for candidates.',
1322
+ trace: [{
1323
+ node: "discovery_fatal",
1324
+ detail: `Discovery failed: ${errMsg}`,
1325
+ data: { error: errMsg },
1326
+ }],
1327
+ };
1328
+ }
1329
+ });
1330
+ }, (result) => {
1331
+ const r = result;
1332
+ if (r?.error)
1333
+ return `error: ${r.error}`;
1334
+ const candidates = r?.candidates;
1335
+ return candidates ? `Found ${candidates.length} candidate(s)` : undefined;
1336
+ });
1337
+ /**
1338
+ * Node 3: Evaluation (Entity bundle)
1339
+ * Builds entity bundle from source + candidates, invokes entity-bundle evaluator, maps to EvaluatedOpportunity with networkId from entities.
1340
+ */
1341
+ const evaluationNode = async (state) => {
1342
+ return timed("OpportunityGraph.evaluation", async () => {
1343
+ const startTime = Date.now();
1344
+ evaluationLog.verbose('Starting evaluation', {
1345
+ candidatesCount: state.candidates.length,
1346
+ });
1347
+ if (state.candidates.length === 0) {
1348
+ evaluationLog.verbose('No candidates to evaluate');
1349
+ return { evaluatedOpportunities: [], agentTimings: [] };
1350
+ }
1351
+ // Batch candidates to avoid timeout - evaluate top 25 per batch, store remaining
1352
+ const EVAL_BATCH_SIZE = 25;
1353
+ const sortedCandidates = [...state.candidates]
1354
+ .sort((a, b) => b.similarity - a.similarity);
1355
+ // Dedup by userId — when same similarity, prefer index with highest relevancyScore
1356
+ const bestByUser = new Map();
1357
+ for (const c of sortedCandidates) {
1358
+ const existing = bestByUser.get(c.candidateUserId);
1359
+ if (!existing) {
1360
+ bestByUser.set(c.candidateUserId, c);
1361
+ }
1362
+ else if (c.similarity > existing.similarity) {
1363
+ bestByUser.set(c.candidateUserId, c);
1364
+ }
1365
+ else if (c.similarity === existing.similarity) {
1366
+ // Tie-break: prefer index with higher relevancy score
1367
+ const cScore = state.indexRelevancyScores[c.networkId] ?? 0;
1368
+ const existingScore = state.indexRelevancyScores[existing.networkId] ?? 0;
1369
+ if (cScore > existingScore) {
1370
+ bestByUser.set(c.candidateUserId, c);
1371
+ }
1372
+ }
1373
+ }
1374
+ const dedupedCandidates = Array.from(bestByUser.values());
1375
+ // Re-sort by similarity descending (Map iteration order doesn't guarantee sort)
1376
+ dedupedCandidates.sort((a, b) => b.similarity - a.similarity);
1377
+ const discoveryUserId = state.onBehalfOfUserId ?? state.userId;
1378
+ let eligibleCandidates;
1379
+ try {
1380
+ const requestedPairs = dedupedCandidates.flatMap((candidate) => [
1381
+ { userId: discoveryUserId, networkId: candidate.networkId },
1382
+ { userId: candidate.candidateUserId, networkId: candidate.networkId },
1383
+ ]);
1384
+ const activePairs = await this.database.getActiveNetworkMembershipPairs(requestedPairs);
1385
+ const activePairKeys = new Set(activePairs.map((pair) => networkMembershipPairKey(pair.userId, pair.networkId)));
1386
+ eligibleCandidates = dedupedCandidates.filter((candidate) => activePairKeys.has(networkMembershipPairKey(discoveryUserId, candidate.networkId))
1387
+ && activePairKeys.has(networkMembershipPairKey(candidate.candidateUserId, candidate.networkId)));
1388
+ }
1389
+ catch (error) {
1390
+ evaluationLog.error('Active network membership recheck failed; skipping evaluation', { error });
1391
+ return {
1392
+ candidates: [],
1393
+ evaluatedOpportunities: [],
1394
+ remainingCandidates: [],
1395
+ error: 'Failed to validate candidate network memberships.',
1396
+ agentTimings: [],
1397
+ };
1398
+ }
1399
+ if (eligibleCandidates.length < dedupedCandidates.length) {
1400
+ evaluationLog.info('Removed candidates without active network pairs before evaluation', {
1401
+ before: dedupedCandidates.length,
1402
+ after: eligibleCandidates.length,
1403
+ removed: dedupedCandidates.length - eligibleCandidates.length,
1404
+ });
1405
+ }
1406
+ if (eligibleCandidates.length === 0) {
1407
+ return { candidates: [], evaluatedOpportunities: [], remainingCandidates: [], agentTimings: [] };
1408
+ }
1409
+ if (dedupedCandidates.length < sortedCandidates.length) {
1410
+ evaluationLog.info("Deduped candidates by userId", {
1411
+ before: sortedCandidates.length,
1412
+ after: dedupedCandidates.length,
1413
+ removed: sortedCandidates.length - dedupedCandidates.length,
1414
+ });
1415
+ }
1416
+ // ── IND-567: Rejection cool-down penalty ────────────────────────────────────────────────────────
1417
+ // Candidates with a recently rejected or stalled opportunity receive a
1418
+ // similarity penalty so they are ranked lower (and often pushed out of
1419
+ // the evaluation batch). This prevents cross-query re-surfacing of
1420
+ // false-positive matches that were already caught downstream.
1421
+ // The persist-node dedup is still the hard gate; this is a soft guard
1422
+ // that reduces evaluator cost and LLM false-positive rate.
1423
+ const rejectionCooldownIds = new Set();
1424
+ if (eligibleCandidates.length > 0
1425
+ && typeof this.database.getRecentlyRejectedOpportunityCounterparties === 'function') {
1426
+ try {
1427
+ const cooldownMs = getRejectionCooldownMs();
1428
+ const ids = await this.database.getRecentlyRejectedOpportunityCounterparties(discoveryUserId, eligibleCandidates.map((c) => c.candidateUserId), cooldownMs);
1429
+ for (const id of ids)
1430
+ rejectionCooldownIds.add(id);
1431
+ if (rejectionCooldownIds.size > 0) {
1432
+ evaluationLog.info('IND-567 rejection cool-down: applying similarity penalty', {
1433
+ affectedCount: rejectionCooldownIds.size,
1434
+ cooldownDays: Math.round(cooldownMs / (24 * 60 * 60 * 1000)),
1435
+ penalty: REJECTION_COOLDOWN_SIMILARITY_PENALTY,
1436
+ });
1437
+ }
1438
+ }
1439
+ catch (err) {
1440
+ evaluationLog.warn('IND-567 rejection cool-down: lookup failed, skipping penalty', {
1441
+ error: err instanceof Error ? err.message : String(err),
1442
+ });
1443
+ }
1444
+ }
1445
+ // Apply penalty and re-sort so penalised candidates fall to the back.
1446
+ const eligibleCandidatesAfterCooldown = rejectionCooldownIds.size > 0
1447
+ ? eligibleCandidates
1448
+ .map((c) => rejectionCooldownIds.has(c.candidateUserId)
1449
+ ? { ...c, similarity: c.similarity * REJECTION_COOLDOWN_SIMILARITY_PENALTY }
1450
+ : c)
1451
+ .sort((a, b) => b.similarity - a.similarity)
1452
+ : eligibleCandidates;
1453
+ const batchToEvaluate = eligibleCandidatesAfterCooldown.slice(0, EVAL_BATCH_SIZE);
1454
+ const remaining = eligibleCandidatesAfterCooldown.slice(EVAL_BATCH_SIZE);
1455
+ // Early termination: if search was query-driven and no query-sourced candidates remain,
1456
+ // clear remaining to prevent pointless pagination through non-query leftovers
1457
+ const isQueryDriven = !!state.searchQuery?.trim();
1458
+ const queryRemaining = remaining.filter((c) => c.discoverySource === 'query' || c.discoverySource == null);
1459
+ const effectiveRemaining = isQueryDriven && queryRemaining.length === 0 ? [] : remaining;
1460
+ if (isQueryDriven && remaining.length > 0 && queryRemaining.length === 0) {
1461
+ evaluationLog.info("Early termination: no query-sourced candidates remain", {
1462
+ droppedCandidates: remaining.length,
1463
+ });
1464
+ }
1465
+ if (effectiveRemaining.length > 0) {
1466
+ evaluationLog.verbose('Batched candidates for evaluation', {
1467
+ evaluating: batchToEvaluate.length,
1468
+ remaining: effectiveRemaining.length,
1469
+ total: sortedCandidates.length,
1470
+ });
1471
+ }
1472
+ const agentTimingsAccum = [];
1473
+ try {
1474
+ const sourceProfile = await this.database.getProfile(discoveryUserId);
1475
+ const sourceEntity = {
1476
+ userId: discoveryUserId,
1477
+ profile: {
1478
+ name: sourceProfile?.identity?.name,
1479
+ bio: sourceProfile?.identity?.bio,
1480
+ location: sourceProfile?.identity?.location,
1481
+ context: sourceProfile?.context,
1482
+ },
1483
+ intents: state.indexedIntents.slice(0, 5).map((i) => ({
1484
+ intentId: i.intentId,
1485
+ payload: i.payload,
1486
+ summary: i.summary,
1487
+ })),
1488
+ networkId: '', // Placeholder — overwritten per-pairing below
1489
+ evidenceKey: `${discoveryUserId}::source`,
1490
+ ragScore: undefined,
1491
+ matchedVia: undefined,
1492
+ };
1493
+ const candidateEntities = await Promise.all(batchToEvaluate.map(async (c) => {
1494
+ const profile = await this.database.getProfile(c.candidateUserId);
1495
+ let intentPayload = c.candidatePayload;
1496
+ let intentSummary = c.candidateSummary;
1497
+ if (c.candidateIntentId != null && (!intentPayload || intentPayload === '')) {
1498
+ const intent = await this.database.getIntent(c.candidateIntentId);
1499
+ if (intent) {
1500
+ intentPayload = intent.payload;
1501
+ intentSummary = intent.summary ?? undefined;
1502
+ }
1503
+ }
1504
+ const evidenceKey = buildEvaluatorEvidenceKey(c);
1505
+ return {
1506
+ userId: c.candidateUserId,
1507
+ profile: {
1508
+ name: profile?.identity?.name,
1509
+ bio: profile?.identity?.bio,
1510
+ location: profile?.identity?.location,
1511
+ context: profile?.context,
1512
+ },
1513
+ intents: c.candidateIntentId != null
1514
+ ? [{ intentId: c.candidateIntentId, payload: intentPayload ?? '', summary: intentSummary }]
1515
+ : undefined,
1516
+ networkId: c.networkId,
1517
+ evidenceKey,
1518
+ ragScore: c.similarity * 100,
1519
+ matchedVia: c.lens,
1520
+ evidence: c.evidence,
1521
+ };
1522
+ }));
1523
+ const userIdToIndexId = new Map();
1524
+ const evidenceByEntityKey = new Map();
1525
+ const entityKeysByUserId = new Map();
1526
+ for (const e of candidateEntities) {
1527
+ if (!userIdToIndexId.has(e.userId))
1528
+ userIdToIndexId.set(e.userId, e.networkId);
1529
+ if (e.evidenceKey) {
1530
+ evidenceByEntityKey.set(e.evidenceKey, mergeOpportunityEvidence(evidenceByEntityKey.get(e.evidenceKey), e.evidence));
1531
+ entityKeysByUserId.set(e.userId, [...(entityKeysByUserId.get(e.userId) ?? []), e.evidenceKey]);
1532
+ }
1533
+ }
1534
+ function evidenceForActor(actor) {
1535
+ if (actor.evidenceKey)
1536
+ return evidenceByEntityKey.get(actor.evidenceKey);
1537
+ const keys = entityKeysByUserId.get(actor.userId) ?? [];
1538
+ const intentKey = actor.intentId ? keys.find((key) => key.endsWith(`:${actor.intentId}`)) : undefined;
1539
+ if (intentKey)
1540
+ return evidenceByEntityKey.get(intentKey);
1541
+ // Avoid leaking unrelated resource evidence when the evaluator collapsed multiple
1542
+ // candidates for the same user into a profile-only actor.
1543
+ if (keys.length === 1)
1544
+ return evidenceByEntityKey.get(keys[0]);
1545
+ return undefined;
1546
+ }
1547
+ // Lower default threshold to 50 for better recall
1548
+ const minScore = state.options.minScore ?? 50;
1549
+ const evaluator = typeof evaluatorAgent.invokeEntityBundle === 'function'
1550
+ ? evaluatorAgent
1551
+ : new OpportunityEvaluator();
1552
+ const runParallel = process.env.RUN_OPPORTUNITY_EVAL_IN_PARALLEL === 'true';
1553
+ const networkContexts = await buildNetworkContexts([sourceEntity, ...candidateEntities], this.database);
1554
+ // Declare trace entries early so both parallel and serial paths can push error entries
1555
+ const traceEntries = [];
1556
+ const parallelErrors = [];
1557
+ let pairwiseOpportunities;
1558
+ if (runParallel) {
1559
+ // Experimental: one LLM call per candidate, all fired in parallel
1560
+ evaluationLog.verbose('Running parallel evaluation', { candidates: candidateEntities.length });
1561
+ const parallelResults = await Promise.all(candidateEntities.map((candidateEntity) => {
1562
+ const input = {
1563
+ discovererId: discoveryUserId,
1564
+ entities: [sourceEntity, candidateEntity],
1565
+ existingOpportunities: state.options.existingOpportunities,
1566
+ ...(state.searchQuery?.trim() ? { discoveryQuery: state.searchQuery.trim() } : {}),
1567
+ networkContexts,
1568
+ };
1569
+ const _evalStart = Date.now();
1570
+ const _traceEmitter = requestContext.getStore()?.traceEmitter;
1571
+ _traceEmitter?.({ type: "agent_start", name: "opportunity-evaluator" });
1572
+ const _candidateName = candidateEntity.profile?.name ?? "Unknown";
1573
+ return evaluator.invokeEntityBundle(input, { minScore, returnAll: true })
1574
+ .then((res) => {
1575
+ const _evalDuration = Date.now() - _evalStart;
1576
+ agentTimingsAccum.push({ name: 'opportunity.evaluator', durationMs: _evalDuration });
1577
+ const _topScore = res.length > 0 ? Math.max(...res.map(r => r.score)) : -1;
1578
+ const _summary = _topScore < 0 ? `${_candidateName}: no match` : `${_candidateName}: ${_topScore}`;
1579
+ _traceEmitter?.({ type: "agent_end", name: "opportunity-evaluator", durationMs: _evalDuration, summary: _summary });
1580
+ return res;
1581
+ })
1582
+ .catch((err) => {
1583
+ const _evalDuration = Date.now() - _evalStart;
1584
+ const _errMsg = err instanceof Error ? err.message : String(err);
1585
+ agentTimingsAccum.push({ name: 'opportunity.evaluator', durationMs: _evalDuration });
1586
+ _traceEmitter?.({ type: "agent_end", name: "opportunity-evaluator", durationMs: _evalDuration, summary: `${_candidateName}: error — ${_errMsg}` });
1587
+ evaluationLog.warn('Parallel eval failed for candidate', {
1588
+ candidateUserId: candidateEntity.userId,
1589
+ error: err,
1590
+ });
1591
+ parallelErrors.push({
1592
+ candidateUserId: candidateEntity.userId,
1593
+ candidateName: _candidateName,
1594
+ error: _errMsg,
1595
+ durationMs: _evalDuration,
1596
+ });
1597
+ return [];
1598
+ });
1599
+ }));
1600
+ // Each call is already pairwise (source + 1 candidate) — flatten directly
1601
+ pairwiseOpportunities = parallelResults.flat();
1602
+ // Record trace entries for candidates that failed during parallel evaluation
1603
+ if (parallelErrors.length > 0) {
1604
+ traceEntries.push({
1605
+ node: "evaluation_errors",
1606
+ detail: `${parallelErrors.length}/${candidateEntities.length} candidate evaluation(s) failed`,
1607
+ data: {
1608
+ failedCount: parallelErrors.length,
1609
+ totalCandidates: candidateEntities.length,
1610
+ errors: parallelErrors.map(e => ({
1611
+ candidateUserId: e.candidateUserId,
1612
+ candidateName: e.candidateName,
1613
+ error: e.error,
1614
+ durationMs: e.durationMs,
1615
+ })),
1616
+ },
1617
+ });
1618
+ }
1619
+ }
1620
+ else {
1621
+ // Default: single bundled LLM call with all candidates
1622
+ const entities = [sourceEntity, ...candidateEntities];
1623
+ const input = {
1624
+ discovererId: discoveryUserId,
1625
+ entities,
1626
+ existingOpportunities: state.options.existingOpportunities,
1627
+ ...(state.searchQuery?.trim() ? { discoveryQuery: state.searchQuery.trim() } : {}),
1628
+ networkContexts,
1629
+ };
1630
+ // Get ALL scored results for tracing (returnAll: true), filter for persistence later
1631
+ const _evalStart = Date.now();
1632
+ const _traceEmitterSerial = requestContext.getStore()?.traceEmitter;
1633
+ _traceEmitterSerial?.({ type: "agent_start", name: "opportunity-evaluator" });
1634
+ let opportunitiesWithActors;
1635
+ try {
1636
+ opportunitiesWithActors = await evaluator.invokeEntityBundle(input, { minScore, returnAll: true });
1637
+ const _evalDuration = Date.now() - _evalStart;
1638
+ agentTimingsAccum.push({ name: 'opportunity.evaluator', durationMs: _evalDuration });
1639
+ _traceEmitterSerial?.({ type: "agent_end", name: "opportunity-evaluator", durationMs: _evalDuration, summary: `Evaluated ${candidateEntities.length} candidate(s)` });
1640
+ }
1641
+ catch (serialErr) {
1642
+ const _evalDuration = Date.now() - _evalStart;
1643
+ const _errMsg = serialErr instanceof Error ? serialErr.message : String(serialErr);
1644
+ agentTimingsAccum.push({ name: 'opportunity.evaluator', durationMs: _evalDuration });
1645
+ _traceEmitterSerial?.({ type: "agent_end", name: "opportunity-evaluator", durationMs: _evalDuration, summary: `error — ${_errMsg}` });
1646
+ throw serialErr; // Re-throw for the outer catch to handle
1647
+ }
1648
+ // Split multi-actor evaluator results into pairwise (viewer + candidate).
1649
+ // Each persisted discovery opportunity should have exactly 2 actors.
1650
+ // When splitting, build per-candidate reasoning from entity data because
1651
+ // the shared reasoning typically describes only one candidate.
1652
+ pairwiseOpportunities = [];
1653
+ for (const op of opportunitiesWithActors) {
1654
+ const pairwiseSourceId = state.onBehalfOfUserId ?? state.userId;
1655
+ const nonViewerActors = op.actors.filter(a => a.userId !== pairwiseSourceId);
1656
+ if (nonViewerActors.length <= 1) {
1657
+ pairwiseOpportunities.push(op);
1658
+ }
1659
+ else {
1660
+ evaluationLog.warn('Splitting multi-actor opportunity; LLM returned bundled actors instead of one-per-candidate', {
1661
+ actorCount: nonViewerActors.length,
1662
+ userIds: nonViewerActors.map(a => a.userId),
1663
+ });
1664
+ const viewerActor = op.actors.find(a => a.userId === pairwiseSourceId);
1665
+ for (const candidate of nonViewerActors) {
1666
+ const entity = candidateEntities.find(e => e.userId === candidate.userId);
1667
+ const candidateName = entity?.profile?.name ?? '';
1668
+ const reasoningLower = op.reasoning.toLowerCase();
1669
+ const mentionsCandidate = candidateName !== '' &&
1670
+ reasoningLower.includes(candidateName.toLowerCase());
1671
+ const mentionsOtherCandidate = nonViewerActors
1672
+ .filter((actor) => actor.userId !== candidate.userId)
1673
+ .map((actor) => candidateEntities.find((e) => e.userId === actor.userId)?.profile?.name?.toLowerCase())
1674
+ .some((name) => name != null && reasoningLower.includes(name));
1675
+ let reasoning;
1676
+ if (mentionsCandidate && !mentionsOtherCandidate) {
1677
+ reasoning = op.reasoning;
1678
+ }
1679
+ else if (entity?.profile) {
1680
+ const p = entity.profile;
1681
+ const parts = [p.name, p.bio].filter(Boolean);
1682
+ if (p.skills?.length)
1683
+ parts.push(`Skills: ${p.skills.join(', ')}`);
1684
+ if (p.interests?.length)
1685
+ parts.push(`Interests: ${p.interests.join(', ')}`);
1686
+ reasoning = parts.join('. ') || op.reasoning;
1687
+ }
1688
+ else {
1689
+ reasoning = op.reasoning;
1690
+ }
1691
+ pairwiseOpportunities.push({
1692
+ reasoning,
1693
+ score: op.score,
1694
+ actors: [
1695
+ viewerActor ?? { userId: pairwiseSourceId, role: 'patient', intentId: null },
1696
+ candidate,
1697
+ ],
1698
+ });
1699
+ }
1700
+ }
1701
+ }
1702
+ }
1703
+ const evaluatedOpportunities = pairwiseOpportunities.map((op) => ({
1704
+ reasoning: op.reasoning,
1705
+ score: op.score,
1706
+ evidence: mergeOpportunityEvidence(...op.actors.map(evidenceForActor)),
1707
+ actors: op.actors.map((a) => {
1708
+ const isSource = a.userId === discoveryUserId;
1709
+ if (isSource) {
1710
+ // Source actor inherits the counterpart's networkId (shared match context)
1711
+ const counterpart = op.actors.find((other) => other.userId !== a.userId);
1712
+ const counterpartIndexId = counterpart
1713
+ ? userIdToIndexId.get(counterpart.userId) ?? candidateEntities.find((e) => e.userId === counterpart.userId)?.networkId
1714
+ : undefined;
1715
+ return {
1716
+ userId: a.userId,
1717
+ role: a.role,
1718
+ intentId: a.intentId,
1719
+ networkId: counterpartIndexId ?? userIdToIndexId.get(a.userId) ?? '',
1720
+ };
1721
+ }
1722
+ return {
1723
+ userId: a.userId,
1724
+ role: a.role,
1725
+ intentId: a.intentId,
1726
+ networkId: userIdToIndexId.get(a.userId) ?? candidateEntities.find((e) => e.userId === a.userId)?.networkId,
1727
+ };
1728
+ }),
1729
+ }));
1730
+ const passed = evaluatedOpportunities.filter((o) => o.score >= minScore);
1731
+ evaluationLog.verbose('Evaluation complete', {
1732
+ evaluatedCount: evaluatedOpportunities.length,
1733
+ passed: passed.length,
1734
+ });
1735
+ // Build detailed trace entries for each evaluated candidate
1736
+ // Threshold filter trace: how many candidates in this batch were above/below similarity threshold
1737
+ const aboveThreshold = batchToEvaluate.filter(c => c.similarity >= 0.40).length;
1738
+ const belowThreshold = batchToEvaluate.length - aboveThreshold;
1739
+ traceEntries.push({
1740
+ node: "threshold_filter",
1741
+ detail: `${aboveThreshold} above 0.40, ${belowThreshold} below (batch of ${batchToEvaluate.length})`,
1742
+ data: {
1743
+ aboveThreshold,
1744
+ belowThreshold,
1745
+ minScore: 0.40,
1746
+ batchSize: batchToEvaluate.length,
1747
+ },
1748
+ });
1749
+ // Create a map of evaluated candidates by userId for quick lookup.
1750
+ // Use discoveryUserId (which accounts for onBehalfOfUserId in introducer flow)
1751
+ // rather than state.userId (which is the introducer, not present in pairwise actors).
1752
+ const evaluatedByUserId = new Map();
1753
+ for (const opp of evaluatedOpportunities) {
1754
+ const candidateActor = opp.actors.find(a => a.userId !== discoveryUserId);
1755
+ if (candidateActor) {
1756
+ evaluatedByUserId.set(candidateActor.userId, { score: opp.score, reasoning: opp.reasoning });
1757
+ }
1758
+ }
1759
+ // Summary entry
1760
+ traceEntries.push({
1761
+ node: "evaluation",
1762
+ detail: `Evaluated ${candidateEntities.length} candidate(s) → ${passed.length} passed (min score ${minScore})`,
1763
+ data: {
1764
+ inputCandidates: batchToEvaluate.length,
1765
+ returnedFromEvaluator: evaluatedOpportunities.length,
1766
+ passedCount: passed.length,
1767
+ minScore,
1768
+ remaining: effectiveRemaining.length,
1769
+ batchNumber: 1,
1770
+ durationMs: Date.now() - startTime,
1771
+ model: getModelName("opportunityEvaluator"),
1772
+ },
1773
+ });
1774
+ // Individual candidate entries - show ALL candidates that went to evaluator
1775
+ for (const entity of candidateEntities) {
1776
+ const candidateName = entity.profile?.name || entity.userId.slice(0, 8);
1777
+ const candidateBio = entity.profile?.bio;
1778
+ const evaluated = evaluatedByUserId.get(entity.userId);
1779
+ const score = evaluated?.score;
1780
+ const reasoning = evaluated?.reasoning;
1781
+ const didPass = score !== undefined && score >= minScore;
1782
+ const status = score !== undefined
1783
+ ? (didPass ? '✓ passed' : `✗ score ${score}`)
1784
+ : '✗ not scored';
1785
+ traceEntries.push({
1786
+ node: "candidate",
1787
+ detail: `${candidateName}: ${status}`,
1788
+ data: {
1789
+ userId: entity.userId,
1790
+ name: candidateName,
1791
+ bio: candidateBio,
1792
+ score: score,
1793
+ passed: didPass,
1794
+ reasoning: reasoning || 'No evaluation returned for this candidate',
1795
+ matchedVia: entity.matchedVia,
1796
+ ragScore: entity.ragScore,
1797
+ model: getModelName("opportunityEvaluator"),
1798
+ intents: entity.intents?.map((i) => ({
1799
+ intentId: i.intentId,
1800
+ summary: (i.summary || i.payload || '').slice(0, 100),
1801
+ })),
1802
+ profile: entity.profile ? {
1803
+ name: entity.profile.name,
1804
+ location: entity.profile.location,
1805
+ } : undefined,
1806
+ },
1807
+ });
1808
+ }
1809
+ // Only pass opportunities that passed the threshold to downstream nodes
1810
+ const passedOpportunities = evaluatedOpportunities.filter((o) => o.score >= minScore);
1811
+ return {
1812
+ candidates: eligibleCandidates,
1813
+ evaluatedOpportunities: passedOpportunities,
1814
+ remainingCandidates: effectiveRemaining,
1815
+ trace: traceEntries,
1816
+ agentTimings: agentTimingsAccum,
1817
+ };
1818
+ }
1819
+ catch (error) {
1820
+ const errMsg = error instanceof Error ? error.message : String(error);
1821
+ evaluationLog.error('Failed', { error });
1822
+ return {
1823
+ evaluatedOpportunities: [],
1824
+ error: 'Failed to evaluate candidates.',
1825
+ trace: [{
1826
+ node: "evaluation_fatal",
1827
+ detail: `Evaluation failed: ${errMsg}`,
1828
+ data: {
1829
+ error: errMsg,
1830
+ candidateCount: state.candidates?.length ?? 0,
1831
+ durationMs: Date.now() - startTime,
1832
+ },
1833
+ }],
1834
+ agentTimings: agentTimingsAccum,
1835
+ };
1836
+ }
1837
+ });
1838
+ };
1839
+ /**
1840
+ * Node 3b: Negotiate (post-persist)
1841
+ * Runs bilateral negotiation per persisted opportunity, passing opportunityId so the
1842
+ * negotiation graph's finalize node updates each opportunity's status:
1843
+ * accept → 'pending' (sender notification follows the pending → notification path)
1844
+ * reject → 'rejected'
1845
+ * timeout/turn_cap → 'stalled'
1846
+ * Status updates land in the DB; in-memory state.opportunities is not mutated.
1847
+ */
1848
+ const NEGOTIATE_TIMER_SENTINEL = Symbol('negotiate-timer-sentinel');
1849
+ const negotiateNode = async (state) => {
1850
+ if (!this.negotiationGraph)
1851
+ return {};
1852
+ if (!state.opportunities || state.opportunities.length === 0)
1853
+ return {};
1854
+ const traceEmitter = requestContext.getStore()?.traceEmitter;
1855
+ const graphStart = Date.now();
1856
+ const persistedById = new Map(state.opportunities.map((opportunity) => [opportunity.id, opportunity]));
1857
+ const attemptBoundaryById = new Map(state.opportunities.map((opportunity) => [opportunity.id, opportunity.updatedAt]));
1858
+ const compensateTasklessNegotiatingOpportunity = async (opportunityId) => {
1859
+ const opportunity = persistedById.get(opportunityId);
1860
+ const expectedUpdatedAt = attemptBoundaryById.get(opportunityId);
1861
+ if (opportunity?.status !== 'negotiating' || !expectedUpdatedAt)
1862
+ return;
1863
+ const fallbackStatus = opportunity.actors.some((actor) => actor.role === 'introducer')
1864
+ ? 'latent'
1865
+ : 'draft';
1866
+ await this.database
1867
+ .compensateTasklessNegotiatingOpportunity(opportunityId, expectedUpdatedAt, fallbackStatus)
1868
+ .catch((error) => {
1869
+ negotiateLog.warn('Failed to compensate taskless negotiating opportunity', {
1870
+ opportunityId,
1871
+ expectedUpdatedAt,
1872
+ fallbackStatus,
1873
+ error,
1874
+ });
1875
+ });
1876
+ };
1877
+ traceEmitter?.({ type: "graph_start", name: "Negotiation graph" });
1878
+ try {
1879
+ // Use the same discoveryUserId pattern as evaluationNode
1880
+ const discoveryUserId = (state.onBehalfOfUserId ?? state.userId);
1881
+ const includeOtherIntents = negotiationIncludesOtherIntents();
1882
+ const sourceAccount = await this.database.getUser(discoveryUserId).catch(() => null);
1883
+ const sourceIntentInputs = (state.indexedIntents ?? []).map((intent) => ({
1884
+ id: intent.intentId,
1885
+ summary: intent.summary ?? null,
1886
+ payload: intent.payload ?? null,
1887
+ }));
1888
+ const sourceHasExactIntent = sourceIntentInputs.some((intent) => intent.id === state.triggerIntentId);
1889
+ const sourceFallbackIntent = state.triggerIntentId && !sourceHasExactIntent
1890
+ ? await this.database.getIntent(state.triggerIntentId).catch(() => null)
1891
+ : null;
1892
+ const ownedSourceFallback = sourceFallbackIntent?.userId === discoveryUserId
1893
+ ? sourceFallbackIntent
1894
+ : null;
1895
+ const sourceUser = {
1896
+ id: discoveryUserId,
1897
+ intents: buildPrioritizedNegotiationIntents(sourceIntentInputs, state.triggerIntentId, ownedSourceFallback, includeOtherIntents),
1898
+ profile: {
1899
+ name: state.sourceProfile?.identity?.name ?? sourceAccount?.name,
1900
+ bio: state.sourceProfile?.identity?.bio ?? sourceAccount?.intro ?? undefined,
1901
+ location: state.sourceProfile?.identity?.location ?? sourceAccount?.location ?? undefined,
1902
+ },
1903
+ };
1904
+ // Build candidates from persisted opportunities. Each opportunity carries its DB id
1905
+ // so the negotiation graph's finalize node can update its status from the outcome.
1906
+ negotiateLog.verbose('Building candidates from opportunities', {
1907
+ opportunityCount: state.opportunities.length,
1908
+ discoveryUserId,
1909
+ });
1910
+ const filteredBeforeInvocation = [];
1911
+ const candidateEntries = state.opportunities
1912
+ .map(opp => {
1913
+ // Skip opportunities where any introducer exists but has not yet approved.
1914
+ const introducerActors = opp.actors
1915
+ .filter(a => a.role === 'introducer');
1916
+ if (introducerActors.length > 0 && !introducerActors.every(a => a.approved === true)) {
1917
+ negotiateLog.verbose('Skipping opportunity: introducer not approved', {
1918
+ opportunityId: opp.id,
1919
+ introducerCount: introducerActors.length,
1920
+ approvedCount: introducerActors.filter(a => a.approved === true).length,
1921
+ });
1922
+ filteredBeforeInvocation.push(opp.id);
1923
+ return null;
1924
+ }
1925
+ const opportunityActors = opp.actors;
1926
+ const sourceActor = opportunityActors.find(a => a.userId === discoveryUserId && a.role !== 'introducer');
1927
+ const candidateActor = opportunityActors.find(a => a.userId !== discoveryUserId && a.role !== 'introducer');
1928
+ if (!sourceActor || !candidateActor) {
1929
+ negotiateLog.verbose('Skipping opportunity: no candidateActor found', {
1930
+ opportunityId: opp.id,
1931
+ discoveryUserId,
1932
+ actors: opp.actors?.map(a => ({ userId: a.userId, role: a.role })) ?? [],
1933
+ });
1934
+ filteredBeforeInvocation.push(opp.id);
1935
+ return null;
1936
+ }
1937
+ return { opp, sourceActor, candidateActor };
1938
+ })
1939
+ .filter((e) => e !== null);
1940
+ await Promise.all(filteredBeforeInvocation.map(compensateTasklessNegotiatingOpportunity));
1941
+ negotiateLog.verbose('Candidate filtering complete', {
1942
+ inputOpportunities: state.opportunities.length,
1943
+ outputCandidates: candidateEntries.length,
1944
+ });
1945
+ const candidates = await Promise.all(candidateEntries.map(async ({ opp, sourceActor, candidateActor }) => {
1946
+ const userId = candidateActor.userId;
1947
+ const sourceIntentId = resolveOpportunityActorIntent(sourceActor);
1948
+ const candidateIntentId = resolveOpportunityActorIntent(candidateActor);
1949
+ const sourceExactActive = sourceIntentId
1950
+ ? sourceIntentInputs.find((sourceIntent) => sourceIntent.id === sourceIntentId)
1951
+ : undefined;
1952
+ const [profile, user, activeIntents, intent, sourceIntent] = await Promise.all([
1953
+ this.database.getProfile(userId).catch(() => null),
1954
+ this.database.getUser(userId).catch(() => null),
1955
+ includeOtherIntents
1956
+ ? this.database.getActiveIntents(userId).catch(() => [])
1957
+ : Promise.resolve([]),
1958
+ candidateIntentId
1959
+ ? this.database.getIntent(candidateIntentId).catch(() => null)
1960
+ : null,
1961
+ sourceIntentId && !sourceExactActive
1962
+ ? this.database.getIntent(sourceIntentId).catch(() => null)
1963
+ : null,
1964
+ ]);
1965
+ const ownedFallbackIntent = intent?.userId === userId ? intent : null;
1966
+ const ownedSourceIntent = sourceIntent?.userId === discoveryUserId ? sourceIntent : null;
1967
+ const candidateIntents = buildPrioritizedNegotiationIntents(activeIntents, candidateIntentId, ownedFallbackIntent, includeOtherIntents);
1968
+ return {
1969
+ userId,
1970
+ ...(sourceIntentId ? { sourceIntentId } : {}),
1971
+ ...(candidateIntentId ? { candidateIntentId } : {}),
1972
+ sourceUser: {
1973
+ ...sourceUser,
1974
+ intents: buildPrioritizedNegotiationIntents(sourceIntentInputs, sourceIntentId, ownedSourceIntent, includeOtherIntents),
1975
+ },
1976
+ opportunityId: opp.id,
1977
+ opportunityStatus: opp.status,
1978
+ opportunityUpdatedAt: opp.updatedAt,
1979
+ reasoning: opp.interpretation?.reasoning ?? '',
1980
+ valencyRole: candidateActor.role ?? 'peer',
1981
+ networkId: candidateActor.networkId,
1982
+ ...(state.searchQuery?.trim() && { discoveryQuery: state.searchQuery.trim() }),
1983
+ candidateUser: {
1984
+ id: userId,
1985
+ intents: candidateIntents,
1986
+ profile: {
1987
+ name: profile?.identity?.name ?? user?.name,
1988
+ bio: profile?.identity?.bio ?? user?.intro ?? undefined,
1989
+ location: profile?.identity?.location ?? user?.location ?? undefined,
1990
+ },
1991
+ },
1992
+ };
1993
+ }));
1994
+ const isChatPath = !!state.options?.conversationId;
1995
+ const maxTurns = isChatPath
1996
+ ? Number(process.env.NEGOTIATION_MAX_TURNS_CHAT) || 4
1997
+ : Number(process.env.NEGOTIATION_MAX_TURNS_AMBIENT) || 6;
1998
+ // Fetch per-candidate index context (group by networkId to avoid duplicate lookups)
1999
+ const uniqueIndexIds = [...new Set(candidates.map(c => c.networkId).filter((id) => !!id))];
2000
+ const indexContextMap = new Map();
2001
+ await Promise.all(uniqueIndexIds.map(async (networkId) => {
2002
+ const ctx = await this.database.getNetworkMemberContext(networkId, discoveryUserId).catch(() => null);
2003
+ const prompt = [ctx?.indexPrompt, ctx?.memberPrompt]
2004
+ .filter((v) => !!v?.trim())
2005
+ .join('\n\n');
2006
+ if (prompt)
2007
+ indexContextMap.set(networkId, prompt);
2008
+ }));
2009
+ // Decide turn timeout.
2010
+ // - Background/queue path (no conversationId): always the park-window
2011
+ // budget (AMBIENT_PARK_WINDOW_MS, 5 min). Turns park in
2012
+ // `waiting_for_agent` and are picked up via polling; the dispatcher
2013
+ // additionally short-circuits to the system agent when no personal
2014
+ // agent has a fresh heartbeat (see AgentDispatcherImpl).
2015
+ // - Chat path with a personal agent authorized: use the same park-window
2016
+ // so the dispatcher parks the turn and the user's personal agent can
2017
+ // pick it up via polling.
2018
+ // - Chat path with no personal agent: use a short timeout (30s) so the
2019
+ // system `Index Negotiator` kicks in without stalling the chat.
2020
+ // Check the personal agent per unique candidate network so cross-network
2021
+ // chat runs don't get a single authorized agent deciding the timeout for
2022
+ // every candidate. Only use the long (polling) timeout when an external
2023
+ // (poller) agent is authorized on ALL candidate networks; otherwise fall
2024
+ // back to the short timeout so chats don't stall on a network where only
2025
+ // the system negotiator is allowed.
2026
+ const hasExternalAgent = isChatPath && this.agentDispatcher
2027
+ ? (uniqueIndexIds.length > 0
2028
+ ? (await Promise.all(uniqueIndexIds.map((networkId) => this.agentDispatcher.hasExternalAgent(discoveryUserId, { action: 'manage:negotiations', scopeType: 'network', scopeId: networkId }).catch(() => false)))).every(Boolean)
2029
+ : false)
2030
+ : false;
2031
+ // Orchestrator (chat-driven a2h) fan-out uses a tight 60s park window —
2032
+ // the user is watching the stream, so we cannot afford the 5-min ambient
2033
+ // budget. Ambient keeps its heartbeat-aware long/short split.
2034
+ const ORCHESTRATOR_PARK_WINDOW_MS = 60000;
2035
+ const isOrchestrator = state.trigger === 'orchestrator';
2036
+ const useLongTimeout = !isChatPath || hasExternalAgent;
2037
+ const timeoutMs = isOrchestrator
2038
+ ? ORCHESTRATOR_PARK_WINDOW_MS
2039
+ : useLongTimeout ? AMBIENT_PARK_WINDOW_MS : 30000;
2040
+ logger.info('negotiateNode timeout decision', {
2041
+ discoveryUserId,
2042
+ trigger: state.trigger,
2043
+ isChatPath,
2044
+ isOrchestrator,
2045
+ hasDispatcher: !!this.agentDispatcher,
2046
+ hasExternalAgent,
2047
+ useLongTimeout,
2048
+ timeoutMs,
2049
+ candidateCount: candidates.length,
2050
+ });
2051
+ // Per-candidate hook — always-on. Accumulates negotiation resolutions
2052
+ // for discovery question generation. Additionally, for the orchestrator
2053
+ // trigger: flips the opp from 'pending' to 'draft' and pushes an
2054
+ // `opportunity_draft_ready` event so the frontend can render it
2055
+ // inline as soon as it resolves, rather than waiting for the full
2056
+ // fan-out. Abort (e.g. user closed the chat) suppresses both the
2057
+ // status flip and the event — the in-flight negotiation finishes
2058
+ // naturally but its card never reaches the user.
2059
+ // Build a stable order index so that resolutions accumulated via the
2060
+ // per-candidate async hook can be re-sorted to candidate-list order
2061
+ // before being handed to buildQuestionPrompt. Without this the LLM
2062
+ // sees negotiations in completion-time order (non-deterministic).
2063
+ const candidateOrderById = new Map();
2064
+ candidates.forEach((c, i) => candidateOrderById.set(c.userId, i));
2065
+ const resolutions = [];
2066
+ const resolvedOpportunityIds = new Set();
2067
+ const onCandidateResolved = async ({ candidate, accepted, turns, outcome }) => {
2068
+ if (candidate.opportunityId)
2069
+ resolvedOpportunityIds.add(candidate.opportunityId);
2070
+ resolutions.push({
2071
+ __order: candidateOrderById.get(candidate.userId) ?? Number.MAX_SAFE_INTEGER,
2072
+ candidateUserId: candidate.userId,
2073
+ counterpartyHint: (() => {
2074
+ const bio = candidate.candidateUser.profile?.bio?.trim();
2075
+ if (bio)
2076
+ return bio;
2077
+ return (candidate.candidateUser.profile?.interests ?? []).join(", ");
2078
+ })(),
2079
+ indexContext: candidate.networkId
2080
+ ? indexContextMap.get(candidate.networkId) ?? ""
2081
+ : "",
2082
+ turns,
2083
+ outcome,
2084
+ });
2085
+ if (candidate.opportunityId) {
2086
+ await compensateTasklessNegotiatingOpportunity(candidate.opportunityId);
2087
+ }
2088
+ if (state.trigger !== 'orchestrator')
2089
+ return;
2090
+ // ─── orchestrator streaming body ───
2091
+ const abortSignal = requestContext.getStore()?.abortSignal;
2092
+ if (abortSignal?.aborted)
2093
+ return;
2094
+ if (!accepted || !candidate.opportunityId)
2095
+ return;
2096
+ // Only emit after a successful status flip — the frontend keys
2097
+ // cards off `opportunity.status === 'draft'`, so emitting a row
2098
+ // with its pre-flip status would render inconsistently. If the
2099
+ // flip fails we log and drop the event; the negotiation result
2100
+ // is still captured in acceptedResults for the final summary.
2101
+ const updated = await this.database
2102
+ .updateOpportunityStatus(candidate.opportunityId, 'draft')
2103
+ .catch((err) => {
2104
+ negotiateLog.warn('failed to flip opp to draft; suppressing draft-ready event', {
2105
+ opportunityId: candidate.opportunityId,
2106
+ error: err,
2107
+ });
2108
+ return null;
2109
+ });
2110
+ if (!updated || abortSignal?.aborted)
2111
+ return;
2112
+ const counterpartName = candidate.candidateUser.profile?.name ?? '';
2113
+ const viewerName = sourceUser.profile.name;
2114
+ const rawReasoning = updated.interpretation?.reasoning ?? '';
2115
+ const personalizedSummary = safeFallbackSummary(rawReasoning, {
2116
+ counterpartName,
2117
+ viewerName,
2118
+ emptyText: 'A suggested connection.',
2119
+ });
2120
+ traceEmitter?.({
2121
+ type: 'opportunity_draft_ready',
2122
+ opportunityId: candidate.opportunityId,
2123
+ opportunity: {
2124
+ ...updated,
2125
+ interpretation: {
2126
+ ...updated.interpretation,
2127
+ reasoning: personalizedSummary,
2128
+ },
2129
+ },
2130
+ personalizedSummary,
2131
+ counterparty: {
2132
+ userId: candidate.candidateUser.id,
2133
+ ...(candidate.candidateUser.profile?.name
2134
+ ? { name: candidate.candidateUser.profile.name }
2135
+ : {}),
2136
+ },
2137
+ });
2138
+ };
2139
+ const negotiationWork = negotiateCandidates(this.negotiationGraph, sourceUser, candidates, { networkId: '', prompt: '' }, // base context, overridden per-candidate below
2140
+ { maxTurns, traceEmitter: traceEmitter ?? undefined,
2141
+ indexContextOverrides: indexContextMap,
2142
+ timeoutMs,
2143
+ trigger: state.trigger === 'orchestrator' ? 'orchestrator' : 'ambient',
2144
+ // v2 initiator stamp: every fresh-discovery origin resolves to the
2145
+ // discovery user — querying user (chat/tool), intent owner
2146
+ // (from-intent), enriched user (from-enrichment/discovery-run), or
2147
+ // represented user (from-introducer, via onBehalfOfUserId).
2148
+ initiatorUserId: discoveryUserId,
2149
+ onCandidateResolved });
2150
+ // MCP-only: race the whole negotiate phase against a budget. When the
2151
+ // timer wins we return early with a `timed_out` trace; the unresolved
2152
+ // promise keeps running in the Bun event loop and each candidate's
2153
+ // finalize node updates its opp status in the DB. We deliberately do
2154
+ // NOT await it, NOT abort it, and NOT mutate state.opportunities —
2155
+ // the MCP tool handler refreshes statuses from the DB before
2156
+ // responding. Bounded blast radius: at most ~20 s of background work
2157
+ // per request; orphans heal via maintenance scripts or IND-279 when
2158
+ // it lands.
2159
+ const budgetMs = state.options.negotiateTimeoutMs;
2160
+ let acceptedResults;
2161
+ if (budgetMs !== undefined) {
2162
+ let timerId;
2163
+ const timerWork = new Promise((resolve) => {
2164
+ timerId = setTimeout(() => resolve(NEGOTIATE_TIMER_SENTINEL), budgetMs);
2165
+ });
2166
+ // try/finally ensures the timer is cleared on every exit path —
2167
+ // sentinel-win, work-win, AND `negotiationWork` rejection. Without
2168
+ // this, a rejected negotiation would leave the timer pending and
2169
+ // keep the event loop alive until `budgetMs` elapses.
2170
+ let raced;
2171
+ try {
2172
+ raced = await Promise.race([negotiationWork, timerWork]);
2173
+ }
2174
+ finally {
2175
+ if (timerId !== undefined)
2176
+ clearTimeout(timerId);
2177
+ }
2178
+ if (raced === NEGOTIATE_TIMER_SENTINEL) {
2179
+ // Restore any attempt that is still before its task boundary. A running or
2180
+ // parked negotiation already has a task and makes the CAS a no-op; a hung
2181
+ // pre-task init becomes owner-actionable while its floating work may retry.
2182
+ await Promise.all(candidates
2183
+ .filter((candidate) => candidate.opportunityId && !resolvedOpportunityIds.has(candidate.opportunityId))
2184
+ .map((candidate) => compensateTasklessNegotiatingOpportunity(candidate.opportunityId)));
2185
+ // Floating promise is intentional — see comment above.
2186
+ void negotiationWork.catch((err) => {
2187
+ negotiateLog.warn('background negotiation failed after timer fired', { error: err });
2188
+ });
2189
+ negotiateLog.warn('timed out — returning partial results to caller', {
2190
+ discoveryUserId,
2191
+ candidateCount: candidates.length,
2192
+ negotiateTimeoutMs: budgetMs,
2193
+ });
2194
+ traceEmitter?.({ type: "graph_end", name: "Negotiation graph", durationMs: Date.now() - graphStart });
2195
+ const orderedResolutionsPartial = [...resolutions]
2196
+ .sort((a, b) => a.__order - b.__order)
2197
+ .map(({ __order: _o, ...r }) => r);
2198
+ const discoveryNegotiationsPartial = orderedResolutionsPartial.map(toDiscoveryNegotiation);
2199
+ const discoverySummaryPartial = buildDiscoverySummary(orderedResolutionsPartial);
2200
+ return {
2201
+ trace: [{
2202
+ node: 'negotiate',
2203
+ detail: 'timed_out',
2204
+ data: {
2205
+ negotiateTimeoutMs: budgetMs,
2206
+ candidateCount: candidates.length,
2207
+ durationMs: Date.now() - graphStart,
2208
+ },
2209
+ }],
2210
+ discoveryNegotiations: discoveryNegotiationsPartial,
2211
+ discoverySummary: discoverySummaryPartial,
2212
+ };
2213
+ }
2214
+ acceptedResults = raced;
2215
+ }
2216
+ else {
2217
+ acceptedResults = await negotiationWork;
2218
+ }
2219
+ // No filtering: every candidate's outcome (accept/reject/stalled) was applied to its
2220
+ // opportunity row by the negotiation graph's finalize node via the opportunityId we
2221
+ // passed. state.opportunities stays as it was at persist time; DB has the new statuses.
2222
+ const acceptedUserIds = new Set(acceptedResults.map(r => r.userId));
2223
+ const negotiationDurationMs = Date.now() - graphStart;
2224
+ const candidateTraceEntries = candidates.map(c => {
2225
+ const accepted = acceptedUserIds.has(c.userId);
2226
+ const result = accepted ? acceptedResults.find(r => r.userId === c.userId) : null;
2227
+ const name = c.candidateUser.profile?.name ?? c.userId;
2228
+ const outcome = accepted ? 'accepted' : 'rejected_or_stalled';
2229
+ return {
2230
+ node: 'negotiate_candidate',
2231
+ detail: `${name}: ${outcome}`,
2232
+ data: {
2233
+ userId: c.userId,
2234
+ opportunityId: c.opportunityId,
2235
+ name,
2236
+ outcome,
2237
+ turns: result?.turnCount ?? 0,
2238
+ },
2239
+ };
2240
+ });
2241
+ const acceptedCount = acceptedResults.length;
2242
+ const otherCount = candidates.length - acceptedCount;
2243
+ const negotiateTrace = [
2244
+ {
2245
+ node: 'negotiate',
2246
+ detail: `${candidates.length} candidate(s) -> ${acceptedCount} accepted, ${otherCount} rejected/stalled`,
2247
+ data: {
2248
+ durationMs: negotiationDurationMs,
2249
+ candidateCount: candidates.length,
2250
+ acceptedCount,
2251
+ otherCount,
2252
+ },
2253
+ },
2254
+ ...candidateTraceEntries,
2255
+ ];
2256
+ traceEmitter?.({ type: "graph_end", name: "Negotiation graph", durationMs: Date.now() - graphStart });
2257
+ const orderedResolutions = [...resolutions]
2258
+ .sort((a, b) => a.__order - b.__order)
2259
+ .map(({ __order: _o, ...r }) => r);
2260
+ const discoveryNegotiations = orderedResolutions.map(toDiscoveryNegotiation);
2261
+ const discoverySummary = buildDiscoverySummary(orderedResolutions);
2262
+ return {
2263
+ trace: negotiateTrace,
2264
+ discoveryNegotiations,
2265
+ discoverySummary,
2266
+ };
2267
+ }
2268
+ catch (err) {
2269
+ await Promise.all(state.opportunities.map((opportunity) => compensateTasklessNegotiatingOpportunity(opportunity.id)));
2270
+ negotiateLog.error("Negotiation stage failed", { error: err });
2271
+ traceEmitter?.({ type: "graph_end", name: "Negotiation graph", durationMs: Date.now() - graphStart });
2272
+ return {
2273
+ trace: [{
2274
+ node: 'negotiate',
2275
+ detail: 'Negotiation failed',
2276
+ data: { durationMs: Date.now() - graphStart, error: true },
2277
+ }],
2278
+ discoveryNegotiations: [],
2279
+ discoverySummary: buildDiscoverySummary([]),
2280
+ };
2281
+ }
2282
+ };
2283
+ /**
2284
+ * Node 4: Ranking
2285
+ * Sorts evaluated opportunities by score, applies limit, dedupes by actor-set hash.
2286
+ */
2287
+ const rankingNode = withNodeTrace("opportunity-ranking", async (state) => {
2288
+ return timed("OpportunityGraph.ranking", async () => {
2289
+ rankingLog.verbose('Starting ranking', {
2290
+ evaluatedCount: state.evaluatedOpportunities.length,
2291
+ });
2292
+ try {
2293
+ const sorted = [...state.evaluatedOpportunities].sort((a, b) => b.score - a.score);
2294
+ const limit = state.options.limit ?? 20;
2295
+ const ranked = sorted.slice(0, limit);
2296
+ const actorSetKey = (opp) => opp.actors
2297
+ .map((a) => `${a.userId}:${a.networkId}`)
2298
+ .sort()
2299
+ .join('|');
2300
+ const seen = new Set();
2301
+ const deduplicated = ranked.filter((opp) => {
2302
+ const key = actorSetKey(opp);
2303
+ if (seen.has(key))
2304
+ return false;
2305
+ seen.add(key);
2306
+ return true;
2307
+ });
2308
+ rankingLog.verbose('Ranking complete', {
2309
+ sorted: sorted.length,
2310
+ afterLimit: ranked.length,
2311
+ afterDedup: deduplicated.length,
2312
+ });
2313
+ return { evaluatedOpportunities: deduplicated };
2314
+ }
2315
+ catch (error) {
2316
+ const errMsg = error instanceof Error ? error.message : String(error);
2317
+ rankingLog.error('Failed', { error });
2318
+ return {
2319
+ evaluatedOpportunities: [],
2320
+ error: 'Failed to rank opportunities.',
2321
+ trace: [{
2322
+ node: "ranking_fatal",
2323
+ detail: `Ranking failed: ${errMsg}`,
2324
+ data: { error: errMsg },
2325
+ }],
2326
+ };
2327
+ }
2328
+ });
2329
+ }, (result) => {
2330
+ const r = result;
2331
+ if (r?.error)
2332
+ return `error: ${r.error}`;
2333
+ const opps = r?.evaluatedOpportunities;
2334
+ return opps ? `Ranked ${opps.length} opportunity(ies)` : undefined;
2335
+ });
2336
+ /**
2337
+ * Node: intro_validation (create_introduction path)
2338
+ * Validates network scope, membership for introducer and all party users, and no existing opportunity.
2339
+ */
2340
+ const introValidationNode = async (state) => {
2341
+ return timed("OpportunityGraph.introValidation", async () => {
2342
+ introValidationLog.verbose('Starting', {
2343
+ userId: state.userId,
2344
+ networkId: state.networkId,
2345
+ entitiesCount: state.introductionEntities?.length ?? 0,
2346
+ });
2347
+ try {
2348
+ const entities = state.introductionEntities ?? [];
2349
+ const primaryNetworkId = (state.networkId ?? entities[0]?.networkId);
2350
+ const partyUserIds = [...new Set(entities.map((e) => e.userId).filter((id) => id !== state.userId))];
2351
+ if (!primaryNetworkId || partyUserIds.length < 1) {
2352
+ return {
2353
+ error: 'Introduction requires networkId and at least two entities (introducer + one counterpart).',
2354
+ };
2355
+ }
2356
+ if (state.requiredNetworkId && primaryNetworkId !== state.requiredNetworkId) {
2357
+ return {
2358
+ error: 'This chat is scoped to a different community. You can only introduce members of the current community.',
2359
+ };
2360
+ }
2361
+ const [introducerIsMember, introducerIsOwner] = await Promise.all([
2362
+ this.database.isNetworkMember(primaryNetworkId, state.userId),
2363
+ this.database.isIndexOwner(primaryNetworkId, state.userId),
2364
+ ]);
2365
+ if (!introducerIsMember && !introducerIsOwner) {
2366
+ return {
2367
+ error: 'One or more users are not members of the specified community. You can only introduce members who share a network.',
2368
+ };
2369
+ }
2370
+ const partyInScope = await Promise.all(partyUserIds.map(async (userId) => {
2371
+ const [isMember, isOwner] = await Promise.all([
2372
+ this.database.isNetworkMember(primaryNetworkId, userId),
2373
+ this.database.isIndexOwner(primaryNetworkId, userId),
2374
+ ]);
2375
+ return isMember || isOwner;
2376
+ }));
2377
+ const allPartyMembers = partyInScope.every(Boolean);
2378
+ if (!allPartyMembers) {
2379
+ return {
2380
+ error: 'One or more users are not members of the specified community. You can only introduce members who share a network.',
2381
+ };
2382
+ }
2383
+ const exists = await this.database.opportunityExistsBetweenActors(partyUserIds, primaryNetworkId);
2384
+ if (exists) {
2385
+ return { error: 'An opportunity already exists between these people.' };
2386
+ }
2387
+ introValidationLog.verbose('Validation passed');
2388
+ return {};
2389
+ }
2390
+ catch (err) {
2391
+ const errMsg = err instanceof Error ? err.message : String(err);
2392
+ introValidationLog.error('Failed', {
2393
+ userId: state.userId,
2394
+ networkId: state.networkId,
2395
+ error: err,
2396
+ });
2397
+ return {
2398
+ error: 'Introduction validation failed.',
2399
+ trace: [{
2400
+ node: "intro_validation_fatal",
2401
+ detail: `IntroValidation failed: ${errMsg}`,
2402
+ data: { error: errMsg },
2403
+ }],
2404
+ };
2405
+ }
2406
+ });
2407
+ };
2408
+ /**
2409
+ * Build fallback reasoning and actors when evaluator returns empty or throws.
2410
+ */
2411
+ function buildIntroFallback(entities, state, primaryNetworkId, introducerName) {
2412
+ const reasoning = `${introducerName ?? 'A member'} believes these people should connect.` +
2413
+ (state.introductionHint ? ` Context: ${state.introductionHint}` : '');
2414
+ const score = 70;
2415
+ const partyUserIds = entities.map((e) => e.userId).filter((id) => id !== state.userId);
2416
+ const actors = partyUserIds.map((uid) => ({
2417
+ userId: uid,
2418
+ role: 'peer',
2419
+ networkId: primaryNetworkId,
2420
+ }));
2421
+ return { reasoning, score, actors };
2422
+ }
2423
+ /**
2424
+ * Node: intro_evaluation (create_introduction path)
2425
+ * Runs entity-bundle evaluator and sets evaluatedOpportunities (one) + introductionContext.
2426
+ */
2427
+ const introEvaluationNode = async (state) => {
2428
+ return timed("OpportunityGraph.introEvaluation", async () => {
2429
+ introEvaluationLog.verbose('Starting', { userId: state.userId });
2430
+ if (state.error) {
2431
+ return { evaluatedOpportunities: [], agentTimings: [] };
2432
+ }
2433
+ const entities = state.introductionEntities ?? [];
2434
+ const primaryNetworkId = (state.networkId ?? entities[0]?.networkId);
2435
+ if (!primaryNetworkId || entities.length < 2) {
2436
+ return { evaluatedOpportunities: [], error: 'Missing entities or network for introduction.', agentTimings: [] };
2437
+ }
2438
+ const agentTimingsAccum = [];
2439
+ let introducerName;
2440
+ let reasoning;
2441
+ let score;
2442
+ let actors = [];
2443
+ const _traceEmitterIntro = requestContext.getStore()?.traceEmitter;
2444
+ let _introEvalStarted = false;
2445
+ let _evalStart = Date.now();
2446
+ try {
2447
+ const introducerUser = await this.database.getUser(state.userId);
2448
+ introducerName = introducerUser?.name ?? undefined;
2449
+ const networkContexts = await buildNetworkContexts(entities, this.database);
2450
+ const input = {
2451
+ discovererId: state.userId,
2452
+ entities,
2453
+ introductionMode: true,
2454
+ introducerName,
2455
+ introductionHint: state.introductionHint ?? undefined,
2456
+ networkContexts,
2457
+ };
2458
+ _evalStart = Date.now();
2459
+ _traceEmitterIntro?.({ type: "agent_start", name: "intro-evaluator" });
2460
+ _introEvalStarted = true;
2461
+ const evaluated = await evaluatorAgent.invokeEntityBundle(input, { minScore: 0 });
2462
+ const _introDuration = Date.now() - _evalStart;
2463
+ agentTimingsAccum.push({ name: 'opportunity.evaluator', durationMs: _introDuration });
2464
+ _traceEmitterIntro?.({ type: "agent_end", name: "intro-evaluator", durationMs: _introDuration, summary: "Evaluated introduction" });
2465
+ if (evaluated.length > 0) {
2466
+ const best = evaluated[0];
2467
+ reasoning = best.reasoning;
2468
+ score = best.score;
2469
+ actors = best.actors.map((a) => ({
2470
+ userId: a.userId,
2471
+ role: a.role,
2472
+ intentId: a.intentId ?? undefined,
2473
+ networkId: primaryNetworkId,
2474
+ }));
2475
+ }
2476
+ else {
2477
+ const fallback = buildIntroFallback(entities, state, primaryNetworkId, introducerName);
2478
+ reasoning = fallback.reasoning;
2479
+ score = fallback.score;
2480
+ actors = fallback.actors;
2481
+ }
2482
+ }
2483
+ catch (evalErr) {
2484
+ const errMsg = evalErr instanceof Error ? evalErr.message : String(evalErr);
2485
+ // Close the intro-evaluator span if it was started before the error
2486
+ if (_introEvalStarted) {
2487
+ const _introErrDuration = Date.now() - _evalStart;
2488
+ _traceEmitterIntro?.({ type: "agent_end", name: "intro-evaluator", durationMs: _introErrDuration, summary: `error — ${errMsg}` });
2489
+ agentTimingsAccum.push({ name: 'opportunity.evaluator', durationMs: _introErrDuration });
2490
+ }
2491
+ introEvaluationLog.warn('Evaluator or getUser failed, using fallback', { error: evalErr });
2492
+ const fallback = buildIntroFallback(entities, state, primaryNetworkId, introducerName);
2493
+ reasoning = fallback.reasoning;
2494
+ score = fallback.score;
2495
+ actors = fallback.actors;
2496
+ return {
2497
+ evaluatedOpportunities: [{ actors, score, reasoning }],
2498
+ introductionContext: { createdByName: introducerName },
2499
+ options: { ...state.options, initialStatus: state.options.initialStatus ?? 'latent' },
2500
+ agentTimings: agentTimingsAccum,
2501
+ trace: [{
2502
+ node: "intro_evaluation_fatal",
2503
+ detail: `IntroEvaluation failed (using fallback): ${errMsg}`,
2504
+ data: { error: errMsg },
2505
+ }],
2506
+ };
2507
+ }
2508
+ const evaluatedOpportunity = {
2509
+ actors,
2510
+ score,
2511
+ reasoning,
2512
+ };
2513
+ return {
2514
+ evaluatedOpportunities: [evaluatedOpportunity],
2515
+ introductionContext: { createdByName: introducerName },
2516
+ options: { ...state.options, initialStatus: state.options.initialStatus ?? 'latent' },
2517
+ agentTimings: agentTimingsAccum,
2518
+ };
2519
+ });
2520
+ };
2521
+ /**
2522
+ * Node 5: Persist
2523
+ * Creates opportunities from evaluator-proposed actors (networkId, userId, role, optional intent).
2524
+ */
2525
+ const persistNode = withNodeTrace("opportunity-persist", async (state) => {
2526
+ return timed("OpportunityGraph.persist", async () => {
2527
+ const startTime = Date.now();
2528
+ const initialStatus = resolveInitialStatus(state.trigger, state.options.initialStatus);
2529
+ persistLog.verbose('Starting persistence (dedup-v2)', {
2530
+ opportunitiesToCreate: state.evaluatedOpportunities.length,
2531
+ trigger: state.trigger,
2532
+ initialStatus,
2533
+ });
2534
+ if (state.evaluatedOpportunities.length === 0) {
2535
+ persistLog.verbose('No opportunities to persist', {
2536
+ triggerIntentId: state.triggerIntentId,
2537
+ reason: state.candidates.length === 0 ? 'no_search_candidates' : 'evaluator_rejected_all',
2538
+ });
2539
+ return {
2540
+ opportunities: [],
2541
+ persistenceOutcome: {
2542
+ evaluatedCount: 0,
2543
+ createdCount: 0,
2544
+ reactivatedCount: 0,
2545
+ sameTriggerDuplicateSuppressions: 0,
2546
+ pairActiveNegotiationSuppressions: 0,
2547
+ crossTriggerAllowedCount: 0,
2548
+ finalAtomicConflictCount: 0,
2549
+ },
2550
+ };
2551
+ }
2552
+ try {
2553
+ const finalTriggerIntentId = state.triggerIntentId;
2554
+ const admission = await admitOpportunityPersistence(this.database, {
2555
+ ownerUserId: state.userId,
2556
+ triggerIntentId: finalTriggerIntentId,
2557
+ networkId: state.networkId,
2558
+ indexScope: state.indexScope,
2559
+ evaluatedOpportunities: state.evaluatedOpportunities,
2560
+ });
2561
+ if (admission.kind === 'empty_scope') {
2562
+ persistLog.info('Skipped persistence because final discovery scope is empty', {
2563
+ userId: state.userId,
2564
+ triggerIntentId: finalTriggerIntentId,
2565
+ });
2566
+ return { opportunities: [] };
2567
+ }
2568
+ const { allowedNetworkIds: finalAllowedNetworkIds, networkEligibility, evaluatedOpportunities: evaluatedToPersist } = admission;
2569
+ if (evaluatedToPersist.length < state.evaluatedOpportunities.length) {
2570
+ persistLog.info('Skipped opportunities with inactive participant network pairs', {
2571
+ before: state.evaluatedOpportunities.length,
2572
+ after: evaluatedToPersist.length,
2573
+ removed: state.evaluatedOpportunities.length - evaluatedToPersist.length,
2574
+ });
2575
+ }
2576
+ if (evaluatedToPersist.length === 0)
2577
+ return { opportunities: [] };
2578
+ const updateStatusIfStillEligible = createEligibleOpportunityStatusUpdater(this.database, finalAllowedNetworkIds, networkEligibility, {
2579
+ onUnavailableAdapter: () => {
2580
+ persistLog.error('Network-eligible status update adapter is unavailable; failing closed');
2581
+ },
2582
+ });
2583
+ const itemsToPersist = [];
2584
+ const reactivatedOpportunities = [];
2585
+ let crossTriggerAllowedCount = 0;
2586
+ const existingBetweenActors = [];
2587
+ const now = new Date().toISOString();
2588
+ // Only skip 'draft' (chat-only) opportunities during dedup.
2589
+ // 'latent' must NOT be skipped — background discovery creates latent opportunities,
2590
+ // and excluding them causes the same user pair to get duplicate opportunities
2591
+ // when multiple intents trigger separate discovery jobs (IND-166).
2592
+ const DEDUP_SKIP_STATUSES = ['draft'];
2593
+ const introducerUserForOnBehalf = state.onBehalfOfUserId
2594
+ ? await this.database.getUser(state.userId)
2595
+ : null;
2596
+ // Orchestrator-only: collect already-accepted pairs so Task 7's
2597
+ // discover_opportunities tool can tell the LLM "these pairs are
2598
+ // already connected, surface the existing chat rather than
2599
+ // creating a new draft". Runs in parallel across unique
2600
+ // counterparties (a single evaluator pass can return multiple
2601
+ // opps per counterparty; we only hit the DB once per pair).
2602
+ // Failures are swallowed — the per-pair query is best-effort.
2603
+ const dedupAlreadyAccepted = [];
2604
+ if (state.trigger === 'orchestrator') {
2605
+ // Use the same viewer-resolution as evaluation/negotiate/persist
2606
+ // on-behalf branches so an introducer-driven orchestrator run
2607
+ // queries accepted opps between the *target* user and the
2608
+ // counterparty, not between the introducer and the counterparty.
2609
+ const dedupUserId = (state.onBehalfOfUserId ?? state.userId);
2610
+ const uniqueCounterparts = new Set();
2611
+ for (const evaluated of evaluatedToPersist) {
2612
+ const candidateUserId = evaluated.actors.find(a => a.userId !== dedupUserId)?.userId;
2613
+ if (candidateUserId)
2614
+ uniqueCounterparts.add(candidateUserId);
2615
+ }
2616
+ const lookups = await Promise.all([...uniqueCounterparts].map(async (counterpartyUserId) => {
2617
+ const accepted = await this.database
2618
+ .findOpportunitiesByActors([dedupUserId, counterpartyUserId], { includeIntroducers: true, statuses: ['accepted'] })
2619
+ .catch((err) => {
2620
+ persistLog.warn('findOpportunitiesByActors (sibling-accept) failed', {
2621
+ userId: dedupUserId,
2622
+ counterpartyUserId,
2623
+ error: err,
2624
+ });
2625
+ return [];
2626
+ });
2627
+ return accepted.map((opp) => ({ opportunityId: opp.id, counterpartyUserId }));
2628
+ }));
2629
+ dedupAlreadyAccepted.push(...lookups.flat());
2630
+ }
2631
+ for (const evaluated of evaluatedToPersist) {
2632
+ const indexIdForActors = state.networkId ?? evaluated.actors[0]?.networkId;
2633
+ let actors;
2634
+ let data;
2635
+ persistPathLog.verbose('Selecting persistence path', {
2636
+ isIntroduction: !!state.introductionContext,
2637
+ stateUserId: state.userId,
2638
+ stateIndexId: state.networkId,
2639
+ evaluatedActorUserIds: evaluated.actors.map(a => a.userId),
2640
+ });
2641
+ if (state.introductionContext) {
2642
+ if (indexIdForActors === undefined) {
2643
+ persistLog.warn('Introduction path missing networkId; skipping opportunity', {
2644
+ userId: state.userId,
2645
+ actorsCount: evaluated.actors.length,
2646
+ });
2647
+ continue;
2648
+ }
2649
+ // Introduction path: manual detection, introducer actor, curator_judgment signal.
2650
+ const evaluatorActors = evaluated.actors.map((a) => {
2651
+ const intent = normalizeOpportunityActorIntent(a.intentId);
2652
+ return {
2653
+ networkId: a.networkId ?? indexIdForActors,
2654
+ userId: a.userId,
2655
+ role: a.role,
2656
+ ...(intent ? { intent: intent } : {}),
2657
+ };
2658
+ });
2659
+ const viewerAlreadyInActors = evaluatorActors.some(a => a.userId === state.userId);
2660
+ actors = viewerAlreadyInActors
2661
+ ? evaluatorActors
2662
+ : [
2663
+ ...evaluatorActors,
2664
+ { networkId: indexIdForActors, userId: state.userId, role: 'introducer', approved: false },
2665
+ ];
2666
+ data = {
2667
+ detection: {
2668
+ source: 'manual',
2669
+ createdBy: state.userId,
2670
+ createdByName: state.introductionContext.createdByName,
2671
+ timestamp: now,
2672
+ },
2673
+ actors,
2674
+ interpretation: {
2675
+ category: 'collaboration',
2676
+ reasoning: evaluated.reasoning,
2677
+ confidence: evaluated.score / 100,
2678
+ signals: [
2679
+ {
2680
+ type: 'curator_judgment',
2681
+ weight: 1,
2682
+ detail: `Introduction by ${state.introductionContext.createdByName ?? 'a member'} via chat`,
2683
+ },
2684
+ ],
2685
+ },
2686
+ context: {
2687
+ networkId: state.networkId ?? indexIdForActors,
2688
+ ...(state.options.conversationId ? { conversationId: state.options.conversationId } : {}),
2689
+ },
2690
+ confidence: String(evaluated.score / 100),
2691
+ status: initialStatus,
2692
+ };
2693
+ }
2694
+ else if (state.onBehalfOfUserId) {
2695
+ if (indexIdForActors === undefined) {
2696
+ persistLog.warn('Introducer discovery path missing networkId; skipping opportunity', {
2697
+ userId: state.userId,
2698
+ actorsCount: evaluated.actors.length,
2699
+ });
2700
+ continue;
2701
+ }
2702
+ // Introducer discovery path: introducer is state.userId, target is onBehalfOfUserId.
2703
+ const evaluatorActors = evaluated.actors.map((a) => {
2704
+ const intent = normalizeOpportunityActorIntent(a.intentId);
2705
+ return {
2706
+ networkId: a.networkId ?? indexIdForActors,
2707
+ userId: a.userId,
2708
+ role: a.role,
2709
+ ...(intent ? { intent: intent } : {}),
2710
+ };
2711
+ });
2712
+ const viewerAlreadyInActors = evaluatorActors.some(a => a.userId === state.userId);
2713
+ actors = viewerAlreadyInActors
2714
+ ? evaluatorActors
2715
+ : [
2716
+ ...evaluatorActors,
2717
+ { networkId: indexIdForActors, userId: state.userId, role: 'introducer', approved: false },
2718
+ ];
2719
+ const candidateUserId = evaluated.actors.find((a) => a.userId !== state.onBehalfOfUserId)?.userId;
2720
+ const overlapping = candidateUserId
2721
+ ? await this.database.findOpportunitiesByActors([state.onBehalfOfUserId, candidateUserId], { excludeStatuses: DEDUP_SKIP_STATUSES })
2722
+ : [];
2723
+ if (overlapping.length > 0) {
2724
+ const existing = overlapping[0];
2725
+ const isRecent = new Date(existing.createdAt).getTime() > Date.now() - DEDUP_WINDOW_MS;
2726
+ const sameIntroducer = existing.actors?.some((actor) => actor.role === 'introducer' && actor.userId === state.userId);
2727
+ if (existing.status === 'expired' || existing.status === 'stalled') {
2728
+ // Reactivate expired or stalled opportunities (only if same introducer for expired).
2729
+ // A different introducer creating for an expired pair falls through to new creation —
2730
+ // the prior expiry belongs to the original introducer, not this one.
2731
+ // Stalled opportunities are reactivated regardless of age: a stalled negotiation
2732
+ // is still in-flight for this pair, so we resume it rather than create a parallel one.
2733
+ if (existing.status === 'stalled' || sameIntroducer) {
2734
+ // Introduction path always targets 'draft' (chat-only surface) rather than using
2735
+ // initialStatus, because introductions are always chat-initiated, not background-discovered.
2736
+ const reactivated = await updateStatusIfStillEligible(existing.id, 'draft', existing.actors, existing.status);
2737
+ if (reactivated) {
2738
+ persistLog.verbose('Reactivated opportunity (introduction path)', {
2739
+ opportunityId: existing.id,
2740
+ candidateUserId,
2741
+ previousStatus: existing.status,
2742
+ });
2743
+ reactivatedOpportunities.push(reactivated);
2744
+ }
2745
+ continue;
2746
+ }
2747
+ }
2748
+ else if (existing.status === 'negotiating') {
2749
+ // Orphan heal (introduction path): same logic as discovery path
2750
+ const priorTask = await this.database.getNegotiationTaskForOpportunity(existing.id);
2751
+ if (priorTask && isActiveNegotiationTaskFresh(priorTask)) {
2752
+ existingBetweenActors.push({
2753
+ candidateUserId: candidateUserId,
2754
+ networkId: (state.networkId ?? indexIdForActors ?? ''),
2755
+ existingOpportunityId: existing.id,
2756
+ existingStatus: existing.status,
2757
+ });
2758
+ persistLog.verbose('Skipping negotiating opportunity with active task (introduction path)', {
2759
+ opportunityId: existing.id,
2760
+ candidateUserId,
2761
+ taskState: priorTask.state,
2762
+ });
2763
+ continue;
2764
+ }
2765
+ const reactivated = await updateStatusIfStillEligible(existing.id, 'draft', existing.actors, existing.status);
2766
+ if (reactivated) {
2767
+ persistLog.info('Resuming orphaned negotiating opportunity (introduction path)', {
2768
+ opportunityId: existing.id,
2769
+ candidateUserId,
2770
+ priorTaskState: priorTask?.state,
2771
+ });
2772
+ reactivatedOpportunities.push(reactivated);
2773
+ }
2774
+ continue;
2775
+ }
2776
+ else if (existing.status === 'latent') {
2777
+ // Upgrade latent to draft for introduction path
2778
+ const upgraded = await updateStatusIfStillEligible(existing.id, 'draft', existing.actors, existing.status);
2779
+ if (upgraded) {
2780
+ persistLog.verbose('Upgraded latent opportunity to draft (introduction path)', {
2781
+ opportunityId: existing.id,
2782
+ candidateUserId,
2783
+ });
2784
+ reactivatedOpportunities.push(upgraded);
2785
+ }
2786
+ continue;
2787
+ }
2788
+ else if (isRecent && candidateUserId) {
2789
+ // Time-gated skip: only skip if opportunity was created within DEDUP_WINDOW_MS
2790
+ existingBetweenActors.push({
2791
+ candidateUserId: candidateUserId,
2792
+ networkId: (state.networkId ?? indexIdForActors ?? ''),
2793
+ existingOpportunityId: existing.id,
2794
+ existingStatus: existing.status,
2795
+ });
2796
+ persistLog.verbose('Skipping recent duplicate (introduction path)', {
2797
+ candidateUserId,
2798
+ existingStatus: existing.status,
2799
+ existingOpportunityId: existing.id,
2800
+ });
2801
+ continue;
2802
+ }
2803
+ // Else: existing opportunity is old enough, allow new opportunity creation
2804
+ persistLog.verbose('Allowing new opportunity; existing is outside dedup window (introduction path)', {
2805
+ candidateUserId,
2806
+ existingStatus: existing.status,
2807
+ existingOpportunityId: existing.id,
2808
+ });
2809
+ }
2810
+ data = {
2811
+ detection: {
2812
+ source: INTRODUCER_DISCOVERY_SOURCE,
2813
+ createdBy: state.userId,
2814
+ createdByName: introducerUserForOnBehalf?.name ?? undefined,
2815
+ timestamp: now,
2816
+ },
2817
+ actors,
2818
+ interpretation: {
2819
+ category: 'collaboration',
2820
+ reasoning: evaluated.reasoning,
2821
+ confidence: evaluated.score / 100,
2822
+ signals: [{
2823
+ type: 'curator_judgment',
2824
+ weight: 1,
2825
+ detail: `Introducer discovery for ${introducerUserForOnBehalf?.name ?? 'a member'} via background maintenance`,
2826
+ }],
2827
+ },
2828
+ context: {
2829
+ networkId: state.networkId ?? indexIdForActors,
2830
+ ...(state.options.conversationId ? { conversationId: state.options.conversationId } : {}),
2831
+ },
2832
+ confidence: String(evaluated.score / 100),
2833
+ status: initialStatus,
2834
+ };
2835
+ }
2836
+ else {
2837
+ // Discovery path: opportunity_graph source, no introducer, lifecycle guard for agent/patient.
2838
+ // Build premise lookup from discovery candidates for premise tracking.
2839
+ // When multiple premise candidates exist for the same user, keep the highest-similarity one.
2840
+ const premiseLookup = new Map();
2841
+ for (const c of state.candidates ?? []) {
2842
+ if (c.candidatePremiseId) {
2843
+ const existing = premiseLookup.get(c.candidateUserId);
2844
+ if (!existing || c.similarity > existing.similarity) {
2845
+ premiseLookup.set(c.candidateUserId, { premiseId: c.candidatePremiseId, similarity: c.similarity });
2846
+ }
2847
+ }
2848
+ }
2849
+ const evaluatorActors = evaluated.actors.map((a) => {
2850
+ const intent = normalizeOpportunityActorIntent(a.intentId);
2851
+ return {
2852
+ networkId: a.networkId ?? indexIdForActors,
2853
+ userId: a.userId,
2854
+ role: a.role,
2855
+ ...(intent ? { intent: intent } : {}),
2856
+ ...(premiseLookup.has(a.userId) ? { premise: premiseLookup.get(a.userId).premiseId } : {}),
2857
+ };
2858
+ });
2859
+ actors = evaluatorActors;
2860
+ const hasIntroducerActor = actors.some(a => a.role === 'introducer');
2861
+ if (!hasIntroducerActor) {
2862
+ const discovererIdx = actors.findIndex(a => a.userId === state.userId);
2863
+ if (discovererIdx >= 0 && actors[discovererIdx].role === 'agent') {
2864
+ const counterpartIdx = actors.findIndex((a, i) => i !== discovererIdx && a.role === 'patient');
2865
+ actors[discovererIdx] = { ...actors[discovererIdx], role: 'patient' };
2866
+ if (counterpartIdx >= 0) {
2867
+ actors[counterpartIdx] = { ...actors[counterpartIdx], role: 'agent' };
2868
+ }
2869
+ persistLog.verbose('Swapped discoverer from agent to patient for lifecycle visibility', {
2870
+ discovererId: state.userId,
2871
+ });
2872
+ }
2873
+ }
2874
+ // Index-agnostic dedup: find ANY existing opportunity between these users,
2875
+ // regardless of which index it was created in or whether a focused network scope is set.
2876
+ const candidateUserId = evaluated.actors.find((a) => a.userId !== state.userId)?.userId;
2877
+ persistDedupLog.verbose('Checking overlapping opportunities', {
2878
+ stateUserId: state.userId,
2879
+ candidateUserId: candidateUserId ?? 'NONE',
2880
+ evaluatedActors: evaluated.actors.map(a => ({ userId: a.userId, role: a.role })),
2881
+ });
2882
+ const overlapping = candidateUserId
2883
+ ? await this.database.findOpportunitiesByActors([state.userId, candidateUserId], { excludeStatuses: DEDUP_SKIP_STATUSES })
2884
+ : [];
2885
+ persistDedupLog.verbose('findOpportunitiesByActors result', {
2886
+ count: overlapping.length,
2887
+ results: overlapping.map(o => ({ id: o.id, status: o.status, actors: o.actors?.map((a) => ({ userId: a.userId, role: a.role })) })),
2888
+ });
2889
+ const ownedIntentTriggerId = state.discoverySource === 'intent'
2890
+ && state.triggerIntentId
2891
+ && state.resolvedTriggerIntentId === state.triggerIntentId
2892
+ ? state.triggerIntentId
2893
+ : undefined;
2894
+ if (ownedIntentTriggerId && candidateUserId) {
2895
+ let activeNegotiation;
2896
+ for (const opportunity of overlapping) {
2897
+ if (opportunity.status !== 'negotiating')
2898
+ continue;
2899
+ const task = await this.database.getNegotiationTaskForOpportunity(opportunity.id);
2900
+ if (task && isActiveNegotiationTaskFresh(task)) {
2901
+ activeNegotiation = { opportunity, taskState: task.state };
2902
+ break;
2903
+ }
2904
+ }
2905
+ if (activeNegotiation) {
2906
+ const existingTriggerIntentId = triggerForOwner(activeNegotiation.opportunity, state.userId);
2907
+ existingBetweenActors.push({
2908
+ candidateUserId: candidateUserId,
2909
+ networkId: (activeNegotiation.opportunity.context?.networkId ?? state.networkId ?? state.userNetworks?.[0] ?? ''),
2910
+ existingOpportunityId: activeNegotiation.opportunity.id,
2911
+ existingStatus: activeNegotiation.opportunity.status,
2912
+ reason: 'pair_active_negotiation',
2913
+ ...(existingTriggerIntentId ? { existingTriggerIntentId } : {}),
2914
+ });
2915
+ persistDedupLog.info('Suppressing owned-intent match for pair-global active negotiation', {
2916
+ triggerIntentId: ownedIntentTriggerId,
2917
+ candidateUserId,
2918
+ existingOpportunityId: activeNegotiation.opportunity.id,
2919
+ existingTriggerIntentId,
2920
+ existingStatus: activeNegotiation.opportunity.status,
2921
+ existingAgeMs: Date.now() - new Date(activeNegotiation.opportunity.createdAt).getTime(),
2922
+ taskState: activeNegotiation.taskState,
2923
+ reason: 'pair_active_negotiation',
2924
+ });
2925
+ continue;
2926
+ }
2927
+ const sameTrigger = overlapping
2928
+ .filter((opportunity) => belongsToOwnedIntent(opportunity, state.userId, ownedIntentTriggerId))
2929
+ .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
2930
+ const otherTrigger = overlapping.filter((opportunity) => !belongsToOwnedIntent(opportunity, state.userId, ownedIntentTriggerId));
2931
+ const existing = sameTrigger[0];
2932
+ if (!existing) {
2933
+ if (otherTrigger.length > 0) {
2934
+ crossTriggerAllowedCount += 1;
2935
+ persistDedupLog.info('Allowing cross-trigger match for owned intent', {
2936
+ triggerIntentId: ownedIntentTriggerId,
2937
+ candidateUserId,
2938
+ reason: 'cross_trigger_match_allowed',
2939
+ otherTriggers: otherTrigger.map((opportunity) => ({
2940
+ opportunityId: opportunity.id,
2941
+ triggerIntentId: triggerForOwner(opportunity, state.userId),
2942
+ status: opportunity.status,
2943
+ ageMs: Date.now() - new Date(opportunity.createdAt).getTime(),
2944
+ })),
2945
+ });
2946
+ }
2947
+ }
2948
+ else {
2949
+ const existingIndexId = (existing.context?.networkId ?? state.networkId ?? state.userNetworks?.[0] ?? '');
2950
+ const isRecent = new Date(existing.createdAt).getTime() > Date.now() - DEDUP_WINDOW_MS;
2951
+ if (existing.status === 'expired' || existing.status === 'stalled') {
2952
+ const reactivated = await updateStatusIfStillEligible(existing.id, initialStatus, existing.actors, existing.status);
2953
+ if (reactivated) {
2954
+ persistLog.info('Reactivated same-trigger opportunity', {
2955
+ triggerIntentId: ownedIntentTriggerId,
2956
+ opportunityId: existing.id,
2957
+ candidateUserId,
2958
+ previousStatus: existing.status,
2959
+ newStatus: initialStatus,
2960
+ });
2961
+ reactivatedOpportunities.push(reactivated);
2962
+ }
2963
+ continue;
2964
+ }
2965
+ if (existing.status === 'negotiating') {
2966
+ const reactivated = await updateStatusIfStillEligible(existing.id, initialStatus, existing.actors, existing.status);
2967
+ if (reactivated) {
2968
+ persistLog.info('Resuming same-trigger orphaned negotiating opportunity', {
2969
+ triggerIntentId: ownedIntentTriggerId,
2970
+ opportunityId: existing.id,
2971
+ candidateUserId,
2972
+ });
2973
+ reactivatedOpportunities.push(reactivated);
2974
+ }
2975
+ continue;
2976
+ }
2977
+ if (existing.status === 'latent' && initialStatus !== 'latent') {
2978
+ const upgraded = await updateStatusIfStillEligible(existing.id, initialStatus, existing.actors, existing.status);
2979
+ if (upgraded) {
2980
+ persistLog.info('Upgraded same-trigger latent opportunity', {
2981
+ triggerIntentId: ownedIntentTriggerId,
2982
+ opportunityId: existing.id,
2983
+ candidateUserId,
2984
+ newStatus: initialStatus,
2985
+ });
2986
+ reactivatedOpportunities.push(upgraded);
2987
+ }
2988
+ continue;
2989
+ }
2990
+ if (isRecent) {
2991
+ existingBetweenActors.push({
2992
+ candidateUserId: candidateUserId,
2993
+ networkId: existingIndexId,
2994
+ existingOpportunityId: existing.id,
2995
+ existingStatus: existing.status,
2996
+ reason: 'same_trigger_recent_duplicate',
2997
+ existingTriggerIntentId: ownedIntentTriggerId,
2998
+ });
2999
+ persistDedupLog.info('Suppressing recent same-trigger duplicate', {
3000
+ triggerIntentId: ownedIntentTriggerId,
3001
+ candidateUserId,
3002
+ existingOpportunityId: existing.id,
3003
+ existingTriggerIntentId: ownedIntentTriggerId,
3004
+ existingStatus: existing.status,
3005
+ existingAgeMs: Date.now() - new Date(existing.createdAt).getTime(),
3006
+ reason: 'same_trigger_recent_duplicate',
3007
+ });
3008
+ continue;
3009
+ }
3010
+ persistDedupLog.info('Allowing same-trigger opportunity outside dedup window', {
3011
+ triggerIntentId: ownedIntentTriggerId,
3012
+ candidateUserId,
3013
+ existingOpportunityId: existing.id,
3014
+ existingStatus: existing.status,
3015
+ existingAgeMs: Date.now() - new Date(existing.createdAt).getTime(),
3016
+ });
3017
+ }
3018
+ }
3019
+ else if (overlapping.length > 0) {
3020
+ const existing = overlapping[0];
3021
+ const existingIndexId = (existing.context?.networkId ?? state.networkId ?? state.userNetworks?.[0] ?? '');
3022
+ const isRecent = new Date(existing.createdAt).getTime() > Date.now() - DEDUP_WINDOW_MS;
3023
+ if (existing.status === 'expired' || existing.status === 'stalled') {
3024
+ // Reactivate expired or stalled opportunities.
3025
+ // Stalled opportunities are reactivated regardless of age: a stalled negotiation
3026
+ // is still in-flight for this pair, so we resume it rather than create a parallel one.
3027
+ const reactivated = await updateStatusIfStillEligible(existing.id, initialStatus, existing.actors, existing.status);
3028
+ if (reactivated) {
3029
+ persistLog.verbose('Reactivated opportunity', {
3030
+ opportunityId: existing.id,
3031
+ candidateUserId,
3032
+ previousStatus: existing.status,
3033
+ newStatus: initialStatus,
3034
+ });
3035
+ reactivatedOpportunities.push(reactivated);
3036
+ }
3037
+ continue;
3038
+ }
3039
+ else if (existing.status === 'negotiating') {
3040
+ // Orphan heal: if a prior opportunity is stuck in 'negotiating' with a stale task,
3041
+ // reactivate it so the new discovery run can reuse it instead of creating a duplicate.
3042
+ const priorTask = await this.database.getNegotiationTaskForOpportunity(existing.id);
3043
+ if (priorTask && isActiveNegotiationTaskFresh(priorTask)) {
3044
+ // Still active — skip (lock gate in init node will handle)
3045
+ existingBetweenActors.push({
3046
+ candidateUserId: candidateUserId,
3047
+ networkId: existingIndexId,
3048
+ existingOpportunityId: existing.id,
3049
+ existingStatus: existing.status,
3050
+ });
3051
+ persistLog.verbose('Skipping negotiating opportunity with active task', {
3052
+ opportunityId: existing.id,
3053
+ candidateUserId,
3054
+ taskState: priorTask.state,
3055
+ });
3056
+ continue;
3057
+ }
3058
+ // Task is stale or missing — reactivate the orphaned negotiating opportunity
3059
+ const reactivated = await updateStatusIfStillEligible(existing.id, initialStatus, existing.actors, existing.status);
3060
+ if (reactivated) {
3061
+ persistLog.info('Resuming orphaned negotiating opportunity', {
3062
+ opportunityId: existing.id,
3063
+ candidateUserId,
3064
+ priorTaskState: priorTask?.state,
3065
+ });
3066
+ reactivatedOpportunities.push(reactivated);
3067
+ }
3068
+ continue;
3069
+ }
3070
+ else if (existing.status === 'latent' && initialStatus !== 'latent') {
3071
+ // Upgrade latent (background-discovered) to the higher-priority status (e.g. pending)
3072
+ const upgraded = await updateStatusIfStillEligible(existing.id, initialStatus, existing.actors, existing.status);
3073
+ if (upgraded) {
3074
+ persistLog.verbose('Upgraded latent opportunity to higher-priority status', {
3075
+ opportunityId: existing.id,
3076
+ candidateUserId,
3077
+ previousStatus: 'latent',
3078
+ newStatus: initialStatus,
3079
+ });
3080
+ reactivatedOpportunities.push(upgraded);
3081
+ }
3082
+ continue;
3083
+ }
3084
+ else if (isRecent && candidateUserId) {
3085
+ // Time-gated skip: only skip if opportunity was created within DEDUP_WINDOW_MS
3086
+ // This prevents parallel job duplicates while allowing new discoveries for long-connected pairs
3087
+ existingBetweenActors.push({
3088
+ candidateUserId: candidateUserId,
3089
+ networkId: existingIndexId,
3090
+ existingOpportunityId: existing.id,
3091
+ existingStatus: existing.status,
3092
+ });
3093
+ persistLog.verbose('Skipping recent duplicate; opportunity created within dedup window', {
3094
+ candidateUserId,
3095
+ existingStatus: existing.status,
3096
+ existingOpportunityId: existing.id,
3097
+ createdAt: existing.createdAt,
3098
+ });
3099
+ continue;
3100
+ }
3101
+ // Else: existing opportunity is old enough (outside the 30-day dedup window), allow new opportunity creation
3102
+ persistLog.verbose('Allowing new opportunity; existing is outside dedup window', {
3103
+ candidateUserId,
3104
+ existingStatus: existing.status,
3105
+ existingOpportunityId: existing.id,
3106
+ createdAt: existing.createdAt,
3107
+ });
3108
+ }
3109
+ data = {
3110
+ detection: {
3111
+ source: 'opportunity_graph',
3112
+ createdBy: 'agent-opportunity-finder',
3113
+ ...(state.discoverySource === 'intent' && state.resolvedTriggerIntentId
3114
+ ? { triggeredBy: state.resolvedTriggerIntentId }
3115
+ : {}),
3116
+ timestamp: now,
3117
+ },
3118
+ actors,
3119
+ interpretation: {
3120
+ category: 'collaboration',
3121
+ reasoning: evaluated.reasoning,
3122
+ confidence: evaluated.score / 100,
3123
+ signals: [
3124
+ {
3125
+ type: evaluated.actors.some((a) => a.intentId) ? 'intent_match' : 'profile_match',
3126
+ weight: evaluated.score / 100,
3127
+ detail: 'Entity-bundle evaluator',
3128
+ },
3129
+ ],
3130
+ },
3131
+ context: {
3132
+ ...(state.networkId ? { networkId: state.networkId } : {}),
3133
+ ...(state.options.conversationId ? { conversationId: state.options.conversationId } : {}),
3134
+ },
3135
+ confidence: String(evaluated.score / 100),
3136
+ status: initialStatus,
3137
+ metadata: {
3138
+ evidence: evaluated.evidence ?? [],
3139
+ },
3140
+ };
3141
+ }
3142
+ if (hasUnsupportedOpportunityClaim(data.interpretation.reasoning)) {
3143
+ persistLog.warn('Skipping opportunity with unsupported affiliation/presence claim at persistence boundary', {
3144
+ source: data.detection.source,
3145
+ triggerIntentId: data.detection.triggeredBy,
3146
+ });
3147
+ continue;
3148
+ }
3149
+ try {
3150
+ validateOpportunityActors(data.actors);
3151
+ }
3152
+ catch (err) {
3153
+ persistLog.warn('Skipping opportunity with invalid actors', {
3154
+ error: err instanceof Error ? err.message : String(err),
3155
+ opportunityReasoning: evaluated.reasoning?.slice(0, 80),
3156
+ });
3157
+ continue;
3158
+ }
3159
+ itemsToPersist.push(data);
3160
+ }
3161
+ const itemsForPersistence = await stampEligibleNewbornOpportunities(itemsToPersist, {
3162
+ ownerUserId: state.userId,
3163
+ operationMode: state.operationMode,
3164
+ hasIntroductionContext: Boolean(state.introductionContext),
3165
+ onBehalfOfUserId: state.onBehalfOfUserId,
3166
+ targetUserId: state.targetUserId,
3167
+ discoverySource: state.discoverySource,
3168
+ resolvedTriggerIntentId: state.resolvedTriggerIntentId,
3169
+ indexedIntentIds: state.indexedIntents.map((intent) => intent.intentId),
3170
+ }, this.stampNewbornOpportunities, {
3171
+ onUnsafeResult: ({ expected, actual }) => {
3172
+ persistLog.warn('Newborn stamper returned unsafe length/order; persisting originals', { expected, actual });
3173
+ },
3174
+ onFailure: ({ intentId, error }) => {
3175
+ persistLog.warn('Newborn stamper failed; persisting originals', {
3176
+ intentId,
3177
+ error: error instanceof Error ? error.message : String(error),
3178
+ });
3179
+ },
3180
+ });
3181
+ const intentDedupScope = finalTriggerIntentId && state.discoverySource === 'intent'
3182
+ ? { triggerIntentId: finalTriggerIntentId, dedupWindowMs: DEDUP_WINDOW_MS }
3183
+ : undefined;
3184
+ const { created: createdList, conflicts } = await persistOpportunities({
3185
+ database: this.database,
3186
+ embedder: this.embedder,
3187
+ items: itemsForPersistence,
3188
+ networkEligibility,
3189
+ intentDedupScope,
3190
+ });
3191
+ for (const conflict of conflicts) {
3192
+ const item = itemsForPersistence[conflict.itemIndex];
3193
+ const candidateActor = item?.actors.find((actor) => actor.userId !== state.userId);
3194
+ if (!candidateActor)
3195
+ continue;
3196
+ existingBetweenActors.push({
3197
+ candidateUserId: candidateActor.userId,
3198
+ networkId: candidateActor.networkId,
3199
+ existingOpportunityId: conflict.existingOpportunityId,
3200
+ existingStatus: conflict.existingStatus,
3201
+ reason: conflict.reason,
3202
+ ...(conflict.existingTriggerIntentId
3203
+ ? { existingTriggerIntentId: conflict.existingTriggerIntentId }
3204
+ : {}),
3205
+ });
3206
+ persistDedupLog.info('Final atomic persistence conflict', {
3207
+ triggerIntentId: finalTriggerIntentId,
3208
+ candidateUserId: candidateActor.userId,
3209
+ existingOpportunityId: conflict.existingOpportunityId,
3210
+ existingTriggerIntentId: conflict.existingTriggerIntentId,
3211
+ existingStatus: conflict.existingStatus,
3212
+ existingAgeMs: Date.now() - new Date(conflict.existingCreatedAt).getTime(),
3213
+ reason: conflict.reason,
3214
+ finalAtomic: true,
3215
+ });
3216
+ }
3217
+ const allOpportunities = [...reactivatedOpportunities, ...createdList];
3218
+ persistLog.verbose('Persistence complete', {
3219
+ created: createdList.length,
3220
+ reactivated: reactivatedOpportunities.length,
3221
+ existingBetweenActorsCount: existingBetweenActors.length,
3222
+ status: initialStatus,
3223
+ });
3224
+ const persistenceOutcome = {
3225
+ evaluatedCount: state.evaluatedOpportunities.length,
3226
+ createdCount: createdList.length,
3227
+ reactivatedCount: reactivatedOpportunities.length,
3228
+ sameTriggerDuplicateSuppressions: existingBetweenActors.filter((entry) => entry.reason === 'same_trigger_recent_duplicate').length,
3229
+ pairActiveNegotiationSuppressions: existingBetweenActors.filter((entry) => entry.reason === 'pair_active_negotiation').length,
3230
+ crossTriggerAllowedCount,
3231
+ finalAtomicConflictCount: conflicts.length,
3232
+ };
3233
+ return {
3234
+ opportunities: allOpportunities,
3235
+ existingBetweenActors,
3236
+ dedupAlreadyAccepted,
3237
+ persistenceOutcome,
3238
+ trace: [{
3239
+ node: "persist",
3240
+ detail: `Created ${createdList.length}, reactivated ${reactivatedOpportunities.length}, ${existingBetweenActors.length} existing skipped, ${dedupAlreadyAccepted.length} already-accepted pair(s)`,
3241
+ data: {
3242
+ created: createdList.length,
3243
+ reactivated: reactivatedOpportunities.length,
3244
+ existingSkipped: existingBetweenActors.length,
3245
+ alreadyAccepted: dedupAlreadyAccepted.length,
3246
+ totalOutput: allOpportunities.length,
3247
+ persistenceOutcome,
3248
+ durationMs: Date.now() - startTime,
3249
+ },
3250
+ }],
3251
+ };
3252
+ }
3253
+ catch (error) {
3254
+ const errMsg = error instanceof Error ? error.message : String(error);
3255
+ persistLog.error('Failed', { error });
3256
+ return {
3257
+ opportunities: [],
3258
+ existingBetweenActors: [],
3259
+ error: 'Failed to persist opportunities.',
3260
+ trace: [{
3261
+ node: "persist_fatal",
3262
+ detail: `Persist failed: ${errMsg}`,
3263
+ data: { error: errMsg },
3264
+ }],
3265
+ };
3266
+ }
3267
+ });
3268
+ }, (result) => {
3269
+ const r = result;
3270
+ if (r?.error)
3271
+ return `error: ${r.error}`;
3272
+ const opps = r?.opportunities;
3273
+ return opps ? `Persisted ${opps.length} opportunity(ies)` : undefined;
3274
+ });
3275
+ // ═══════════════════════════════════════════════════════════════
3276
+ // CRUD NODES (read, update, delete, send)
3277
+ // ═══════════════════════════════════════════════════════════════
3278
+ /**
3279
+ * Read Node: List opportunities for the user, optionally filtered by networkId.
3280
+ * Fast path — no LLM calls.
3281
+ */
3282
+ const readNode = async (state) => {
3283
+ return timed("OpportunityGraph.read", async () => {
3284
+ readLog.verbose('Listing opportunities', {
3285
+ userId: state.userId,
3286
+ networkId: state.networkId,
3287
+ });
3288
+ try {
3289
+ let indexIdFilter;
3290
+ if (state.networkId) {
3291
+ const [isMember, isOwner] = await Promise.all([
3292
+ this.database.isNetworkMember(state.networkId, state.userId),
3293
+ this.database.isIndexOwner(state.networkId, state.userId),
3294
+ ]);
3295
+ if (!isMember && !isOwner) {
3296
+ return {
3297
+ readResult: { count: 0, opportunities: [], message: 'Network not found or you are not a member.' },
3298
+ };
3299
+ }
3300
+ indexIdFilter = state.networkId;
3301
+ }
3302
+ const rawList = await this.database.getOpportunitiesForUser(state.userId, {
3303
+ limit: 30,
3304
+ ...(indexIdFilter ? { networkId: indexIdFilter } : {}),
3305
+ });
3306
+ const list = rawList.filter((opp) => opp.status !== 'expired');
3307
+ if (list.length === 0) {
3308
+ return {
3309
+ readResult: {
3310
+ count: 0,
3311
+ message: 'You have no opportunities yet. Use discover_opportunities to search for connections.',
3312
+ opportunities: [],
3313
+ },
3314
+ };
3315
+ }
3316
+ // Dedupe by counterpart set (same people = one row) so chat does not show "You and X" per index
3317
+ const counterpartKey = (opp) => opp.actors
3318
+ .filter((a) => a.userId !== state.userId && a.role !== 'introducer')
3319
+ .map((a) => a.userId)
3320
+ .sort()
3321
+ .join(',');
3322
+ const byKey = new Map();
3323
+ for (const opp of list) {
3324
+ const key = counterpartKey(opp);
3325
+ const existing = byKey.get(key);
3326
+ const conf = Number(opp.interpretation?.confidence ?? opp.confidence ?? 0);
3327
+ const existingConf = existing ? Number(existing.interpretation?.confidence ?? existing.confidence ?? 0) : 0;
3328
+ const oppTime = opp.updatedAt instanceof Date ? opp.updatedAt.getTime() : new Date(opp.updatedAt).getTime();
3329
+ const existingTime = existing
3330
+ ? (existing.updatedAt instanceof Date ? existing.updatedAt.getTime() : new Date(existing.updatedAt).getTime())
3331
+ : 0;
3332
+ if (!existing || conf > existingConf || (conf === existingConf && oppTime > existingTime)) {
3333
+ byKey.set(key, opp);
3334
+ }
3335
+ }
3336
+ const dedupedList = [...byKey.values()];
3337
+ const sourceLabel = {
3338
+ chat: 'Suggested in chat',
3339
+ opportunity_graph: 'System match',
3340
+ manual: 'Manual',
3341
+ cron: 'Scheduled',
3342
+ member_added: 'Member added',
3343
+ introducer_discovery: 'Suggested by contact',
3344
+ };
3345
+ const enriched = await Promise.all(dedupedList.map(async (opp) => {
3346
+ // "Other parties" = all actors who are not the current user (exclude introducer for suggestedBy).
3347
+ // Opportunity graph persists roles as 'agent'|'patient'|'peer'; manual/createManual use 'party'.
3348
+ const otherParties = opp.actors.filter((a) => a.userId !== state.userId && a.role !== 'introducer');
3349
+ const introducer = opp.actors.find((a) => a.role === 'introducer');
3350
+ const partyIds = otherParties.map((a) => a.userId);
3351
+ const idsToResolve = introducer ? [...partyIds, introducer.userId] : partyIds;
3352
+ // Use the counterpart's (non-viewer) networkId — it reflects where the match was found.
3353
+ // actors[0] is typically the viewer with an arbitrary first-target-index value.
3354
+ const counterpartActor = opp.actors.find((a) => a.userId !== state.userId);
3355
+ const actorIndexId = counterpartActor?.networkId ?? opp.actors[0]?.networkId;
3356
+ const [indexRecord, ...profileAndUserPairs] = await Promise.all([
3357
+ actorIndexId ? this.database.getNetwork(actorIndexId) : Promise.resolve(null),
3358
+ ...idsToResolve.map(async (uid) => {
3359
+ const [profile, user] = await Promise.all([
3360
+ this.database.getProfile(uid),
3361
+ this.database.getUser(uid),
3362
+ ]);
3363
+ return (profile?.identity?.name ?? user?.name ?? 'Unknown');
3364
+ }),
3365
+ ]);
3366
+ const connectedWith = profileAndUserPairs.slice(0, partyIds.length);
3367
+ const suggestedBy = introducer ? profileAndUserPairs[partyIds.length] ?? null : null;
3368
+ const category = opp.interpretation?.category ?? 'connection';
3369
+ const confidence = opp.interpretation?.confidence ?? (opp.confidence ? Number(opp.confidence) : null);
3370
+ const source = opp.detection?.source ? (sourceLabel[opp.detection.source] ?? opp.detection.source) : null;
3371
+ return {
3372
+ id: opp.id,
3373
+ indexName: indexRecord?.title ?? (actorIndexId ?? ''),
3374
+ connectedWith,
3375
+ suggestedBy,
3376
+ reasoning: safeFallbackSummary(opp.interpretation?.reasoning, {
3377
+ counterpartName: connectedWith.join(' and '),
3378
+ emptyText: 'Connection opportunity',
3379
+ }),
3380
+ status: opp.status,
3381
+ category,
3382
+ confidence: confidence != null ? confidence : null,
3383
+ source,
3384
+ };
3385
+ }));
3386
+ return {
3387
+ readResult: {
3388
+ count: enriched.length,
3389
+ message: `You have ${enriched.length} opportunity(ies).`,
3390
+ opportunities: enriched,
3391
+ },
3392
+ };
3393
+ }
3394
+ catch (err) {
3395
+ readLog.error('Failed', { error: err });
3396
+ return {
3397
+ readResult: { count: 0, opportunities: [], message: 'Failed to list opportunities.' },
3398
+ };
3399
+ }
3400
+ });
3401
+ };
3402
+ /**
3403
+ * Update Node: Change opportunity status (accept, reject, etc.).
3404
+ * For 'accepted', enforces the self-accept guard: the caller's actor entry
3405
+ * must not already have `actedAt` set — i.e. the caller has not yet been
3406
+ * the one to advance this opportunity's state. Stamps `actedAt` on accept
3407
+ * atomically with the status change via `stampOpportunityActorAction`.
3408
+ */
3409
+ const updateNode = async (state) => {
3410
+ return timed("OpportunityGraph.update", async () => {
3411
+ updateLog.verbose('Updating opportunity status', {
3412
+ userId: state.userId,
3413
+ opportunityId: state.opportunityId,
3414
+ newStatus: state.newStatus,
3415
+ });
3416
+ try {
3417
+ return {
3418
+ mutationResult: await updateOpportunityLifecycle(this.database, {
3419
+ opportunityId: state.opportunityId,
3420
+ actorUserId: state.userId,
3421
+ newStatus: state.newStatus,
3422
+ }),
3423
+ };
3424
+ }
3425
+ catch (err) {
3426
+ updateLog.error('Failed', { error: err });
3427
+ return { mutationResult: { success: false, error: 'Failed to update opportunity.' } };
3428
+ }
3429
+ });
3430
+ };
3431
+ /**
3432
+ * Delete Node: Expire/archive an opportunity.
3433
+ */
3434
+ const deleteNode = async (state) => {
3435
+ return timed("OpportunityGraph.delete", async () => {
3436
+ deleteLog.verbose('Expiring opportunity', {
3437
+ userId: state.userId,
3438
+ opportunityId: state.opportunityId,
3439
+ });
3440
+ try {
3441
+ return {
3442
+ mutationResult: await deleteOpportunityLifecycle(this.database, {
3443
+ opportunityId: state.opportunityId,
3444
+ actorUserId: state.userId,
3445
+ }),
3446
+ };
3447
+ }
3448
+ catch (err) {
3449
+ deleteLog.error('Failed', { error: err });
3450
+ return { mutationResult: { success: false, error: 'Failed to delete opportunity.' } };
3451
+ }
3452
+ });
3453
+ };
3454
+ /**
3455
+ * Send Node: Promote latent or draft opportunity to pending + queue notification.
3456
+ */
3457
+ const sendNode = async (state) => {
3458
+ return timed("OpportunityGraph.send", async () => {
3459
+ sendLog.verbose('Sending opportunity', {
3460
+ userId: state.userId,
3461
+ opportunityId: state.opportunityId,
3462
+ });
3463
+ try {
3464
+ return {
3465
+ mutationResult: await sendOpportunityLifecycle(this.database, { opportunityId: state.opportunityId, actorUserId: state.userId }, this.queueNotification),
3466
+ };
3467
+ }
3468
+ catch (err) {
3469
+ sendLog.error('Failed', { error: err });
3470
+ return { mutationResult: { success: false, error: 'Failed to send opportunity.' } };
3471
+ }
3472
+ });
3473
+ };
3474
+ /**
3475
+ * Negotiate Existing Node: Load an existing opportunity by ID and run bilateral negotiation.
3476
+ * Used after introducer approval to trigger the normal negotiation flow for a latent opportunity.
3477
+ */
3478
+ const negotiateExistingNode = async (state) => {
3479
+ if (!state.opportunityId)
3480
+ return {};
3481
+ if (!this.negotiationGraph) {
3482
+ negotiateExistingLog.warn('No negotiationGraph wired; skipping', {
3483
+ opportunityId: state.opportunityId,
3484
+ });
3485
+ return {};
3486
+ }
3487
+ try {
3488
+ const continuation = state.options.negotiationContinuation;
3489
+ const result = await negotiateExistingOpportunity(this.database, async ({ sourceUser, candidate, indexContextOverrides, continuation: execution }) => {
3490
+ let receipt;
3491
+ // Deliberately no `initiatorUserId`: re-entries inherit the prior task's
3492
+ // stamped seat in negotiation initialization, never re-deriving it here.
3493
+ const acceptedResults = await negotiateCandidates(this.negotiationGraph, sourceUser, [candidate], { networkId: '', prompt: '' }, {
3494
+ maxTurns: Number(process.env.NEGOTIATION_MAX_TURNS_AMBIENT) || 6,
3495
+ indexContextOverrides,
3496
+ timeoutMs: AMBIENT_PARK_WINDOW_MS,
3497
+ trigger: 'ambient',
3498
+ ...(execution ? {
3499
+ resumeFromTaskId: execution.taskId,
3500
+ continuationSettlementId: execution.settlementId,
3501
+ continuationExecution: execution,
3502
+ onCandidateResolved: async ({ continuationReceipt }) => {
3503
+ if (continuationReceipt?.successorTaskId === execution.successorTaskId)
3504
+ receipt = continuationReceipt;
3505
+ },
3506
+ } : {}),
3507
+ });
3508
+ return { accepted: acceptedResults.length > 0, ...(receipt ? { receipt } : {}) };
3509
+ }, { opportunityId: state.opportunityId, actorUserId: state.userId, continuation }, this.queueNotification, {
3510
+ onNotificationFailure: ({ actorId, error }) => {
3511
+ negotiateExistingLog.warn('Failed to queue notification', { actorId, error });
3512
+ },
3513
+ });
3514
+ if (result.kind === 'skipped') {
3515
+ const messages = {
3516
+ not_found: 'Opportunity not found',
3517
+ stale_continuation: 'Exact continuation actor binding is stale',
3518
+ no_source_actor: 'No source actor found',
3519
+ no_candidate_actor: 'No candidate actor found',
3520
+ };
3521
+ negotiateExistingLog.warn(messages[result.reason], {
3522
+ opportunityId: state.opportunityId,
3523
+ ...(result.reason === 'stale_continuation' && continuation ? { taskId: continuation.taskId } : {}),
3524
+ });
3525
+ return {};
3526
+ }
3527
+ negotiateExistingLog.info('Negotiation complete', {
3528
+ opportunityId: result.opportunityId,
3529
+ accepted: result.accepted,
3530
+ continuationFence: result.continuationFence,
3531
+ });
3532
+ return result.receipt ? { negotiationContinuationReceipt: result.receipt } : {};
3533
+ }
3534
+ catch (err) {
3535
+ negotiateExistingLog.error('Failed', { opportunityId: state.opportunityId, error: err });
3536
+ return { error: `Failed to load opportunity: ${err instanceof Error ? err.message : String(err)}` };
3537
+ }
3538
+ return {};
3539
+ };
3540
+ /**
3541
+ * Node: Approve Introduction
3542
+ * Called by the introducer to approve a latent introducer-pattern opportunity.
3543
+ * Sets approved=true on the introducer actor (status stays latent), then
3544
+ * enqueues a negotiate_existing job so the parties negotiate normally.
3545
+ */
3546
+ const approveIntroductionNode = async (state) => {
3547
+ return {
3548
+ mutationResult: await approveOpportunityIntroduction(this.database, { opportunityId: state.opportunityId, actorUserId: state.userId }, this.queueNegotiateExisting),
3549
+ };
3550
+ };
3551
+ // ═══════════════════════════════════════════════════════════════
3552
+ // CONDITIONAL ROUTING FUNCTIONS
3553
+ // ═══════════════════════════════════════════════════════════════
3554
+ /**
3555
+ * Router: Decides which path based on operationMode.
3556
+ */
3557
+ const routeByMode = (state) => {
3558
+ const mode = state.operationMode ?? 'create';
3559
+ if (mode === 'read')
3560
+ return 'read';
3561
+ if (mode === 'update')
3562
+ return 'update';
3563
+ if (mode === 'delete')
3564
+ return 'delete_opp';
3565
+ if (mode === 'send')
3566
+ return 'send';
3567
+ if (mode === 'create_introduction')
3568
+ return 'intro_validation';
3569
+ if (mode === 'negotiate_existing')
3570
+ return 'negotiate_existing';
3571
+ if (mode === 'approve_introduction')
3572
+ return 'approve_introduction';
3573
+ // 'create' is the default discovery pipeline
3574
+ return 'prep';
3575
+ };
3576
+ /**
3577
+ * After prep: check if user has indexed intents.
3578
+ * Early exit if none (cannot find opportunities).
3579
+ */
3580
+ const shouldContinueAfterPrep = (state) => {
3581
+ if (state.error) {
3582
+ routingLog.verbose('Error in prep - ending early');
3583
+ return END;
3584
+ }
3585
+ // Continuation mode: skip scope/resolve/discovery, go straight to evaluation
3586
+ if (state.operationMode === 'continue_discovery') {
3587
+ routingLog.verbose('Continue discovery → skipping to evaluation', {
3588
+ candidatesLoaded: state.candidates.length,
3589
+ });
3590
+ return 'evaluation';
3591
+ }
3592
+ routingLog.verbose('Continuing to scope');
3593
+ return 'scope';
3594
+ };
3595
+ /**
3596
+ * After scope: check if we have target indexes.
3597
+ */
3598
+ const shouldContinueAfterScope = (state) => {
3599
+ if (state.error || state.targetNetworks.length === 0) {
3600
+ routingLog.verbose('No target indexes - ending early');
3601
+ return END;
3602
+ }
3603
+ routingLog.verbose('Continuing to resolve');
3604
+ return 'resolve';
3605
+ };
3606
+ /**
3607
+ * After discovery: if create-intent signal was set, end so tool can return it; else continue to evaluation.
3608
+ */
3609
+ const shouldContinueAfterDiscovery = (state) => {
3610
+ if (state.createIntentSuggested) {
3611
+ routingLog.verbose('Create-intent suggested - ending for tool signal');
3612
+ return END;
3613
+ }
3614
+ return 'evaluation';
3615
+ };
3616
+ /**
3617
+ * After intro_validation: if validation set state.error, end early; else continue to intro_evaluation.
3618
+ */
3619
+ const routeAfterIntroValidation = (state) => {
3620
+ if (state.error) {
3621
+ routingLog.verbose('Intro validation error - ending early');
3622
+ return END;
3623
+ }
3624
+ return 'intro_evaluation';
3625
+ };
3626
+ // ═══════════════════════════════════════════════════════════════
3627
+ // GRAPH ASSEMBLY
3628
+ // ═══════════════════════════════════════════════════════════════
3629
+ const workflow = new StateGraph(OpportunityGraphState)
3630
+ // Add all nodes
3631
+ .addNode('prep', prepNode)
3632
+ .addNode('scope', scopeNode)
3633
+ .addNode('resolve', resolveNode)
3634
+ .addNode('discovery', discoveryNode)
3635
+ .addNode('evaluation', evaluationNode)
3636
+ .addNode('ranking', rankingNode)
3637
+ .addNode('intro_validation', introValidationNode)
3638
+ .addNode('intro_evaluation', introEvaluationNode)
3639
+ .addNode('persist', persistNode)
3640
+ // CRUD nodes
3641
+ .addNode('read', readNode)
3642
+ .addNode('update', updateNode)
3643
+ .addNode('delete_opp', deleteNode)
3644
+ .addNode('send', sendNode)
3645
+ .addNode('negotiate_existing', negotiateExistingNode)
3646
+ .addNode('approve_introduction', approveIntroductionNode)
3647
+ // Route by operation mode from START
3648
+ .addConditionalEdges(START, routeByMode, {
3649
+ prep: 'prep',
3650
+ intro_validation: 'intro_validation',
3651
+ read: 'read',
3652
+ update: 'update',
3653
+ delete_opp: 'delete_opp',
3654
+ send: 'send',
3655
+ negotiate_existing: 'negotiate_existing',
3656
+ approve_introduction: 'approve_introduction',
3657
+ })
3658
+ // Introduction path: validation -> evaluation -> persist (or END on validation error)
3659
+ .addConditionalEdges('intro_validation', routeAfterIntroValidation, {
3660
+ intro_evaluation: 'intro_evaluation',
3661
+ [END]: END,
3662
+ })
3663
+ .addEdge('intro_evaluation', 'persist')
3664
+ // CRUD fast paths -> END
3665
+ .addEdge('read', END)
3666
+ .addEdge('update', END)
3667
+ .addEdge('delete_opp', END)
3668
+ .addEdge('send', END)
3669
+ .addEdge('negotiate_existing', END)
3670
+ .addEdge('approve_introduction', END)
3671
+ // Conditional routing: early exit if no indexed intents
3672
+ .addConditionalEdges('prep', shouldContinueAfterPrep, {
3673
+ scope: 'scope',
3674
+ evaluation: 'evaluation',
3675
+ [END]: END,
3676
+ })
3677
+ // Conditional routing: early exit if no target indexes
3678
+ .addConditionalEdges('scope', shouldContinueAfterScope, {
3679
+ resolve: 'resolve',
3680
+ [END]: END,
3681
+ })
3682
+ .addEdge('resolve', 'discovery')
3683
+ .addConditionalEdges('discovery', shouldContinueAfterDiscovery, {
3684
+ evaluation: 'evaluation',
3685
+ [END]: END,
3686
+ })
3687
+ // Discovery → Ranking → Persist → Negotiate (post-persist).
3688
+ // Fresh and continuation discovery both negotiate newly created/reactivated
3689
+ // opportunities. The stage is skipped only when no negotiation graph is wired or
3690
+ // persistence produced no negotiation targets (negotiateNode also guards both cases).
3691
+ .addNode('negotiate', negotiateNode)
3692
+ .addEdge('evaluation', 'ranking')
3693
+ .addEdge('ranking', 'persist')
3694
+ .addConditionalEdges('persist', (state) => {
3695
+ if (!this.negotiationGraph)
3696
+ return END;
3697
+ if (!state.opportunities || state.opportunities.length === 0)
3698
+ return END;
3699
+ return 'negotiate';
3700
+ }, {
3701
+ negotiate: 'negotiate',
3702
+ [END]: END,
3703
+ })
3704
+ .addEdge('negotiate', END);
3705
+ return workflow.compile();
3706
+ }
3707
+ }