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