@@ -24,7 +24,7 @@ export function useWorkspaces(page = 1, pageSize = 20) {
2424 queryKey : QK . workspaces ( page , pageSize ) ,
2525 queryFn : async ( ) => {
2626 const { data, error } = await client . current . POST ( "/v3/workspaces/list" , {
27- params : { query : { page, page_size : pageSize } } ,
27+ params : { query : { page, size : pageSize } } ,
2828 body : { } ,
2929 } ) ;
3030 return data ?? err ( error ) ;
@@ -149,7 +149,7 @@ export function usePeers(workspaceId: string, page = 1, pageSize = 20) {
149149 const { data, error } = await client . current . POST (
150150 "/v3/workspaces/{workspace_id}/peers/list" ,
151151 {
152- params : { path : { workspace_id : workspaceId } , query : { page, page_size : pageSize } } ,
152+ params : { path : { workspace_id : workspaceId } , query : { page, size : pageSize } } ,
153153 body : { } ,
154154 } ,
155155 ) ;
@@ -263,7 +263,7 @@ export function usePeerSessions(workspaceId: string, peerId: string, page = 1, p
263263 {
264264 params : {
265265 path : { workspace_id : workspaceId , peer_id : peerId } ,
266- query : { page, page_size : pageSize } ,
266+ query : { page, size : pageSize } ,
267267 } ,
268268 body : { } ,
269269 } ,
@@ -338,7 +338,7 @@ export function useSessions(workspaceId: string, page = 1, pageSize = 20) {
338338 {
339339 params : {
340340 path : { workspace_id : workspaceId } ,
341- query : { page, page_size : pageSize } ,
341+ query : { page, size : pageSize } ,
342342 } ,
343343 body : { } ,
344344 } ,
@@ -427,7 +427,7 @@ export function useSessionMessages(
427427 {
428428 params : {
429429 path : { workspace_id : workspaceId , session_id : sessionId } ,
430- query : { page, page_size : pageSize } ,
430+ query : { page, size : pageSize } ,
431431 } ,
432432 body : { } ,
433433 } ,
@@ -642,6 +642,11 @@ export function useSessionContext(workspaceId: string, sessionId: string) {
642642
643643// ─── Conclusions ──────────────────────────────────────────────────────────────
644644
645+ // Target-discovery walk bounds (see useConclusionTargetPeers): enough for
646+ // realistically sized per-observer stores; discovery is a read-only listing.
647+ const CONCLUSION_TARGET_PAGE_SIZE = 100 ;
648+ const CONCLUSION_TARGET_MAX_PAGES = 40 ;
649+
645650export function useConclusions (
646651 workspaceId : string ,
647652 filters : Record < string , unknown > = { } ,
@@ -657,7 +662,7 @@ export function useConclusions(
657662 {
658663 params : {
659664 path : { workspace_id : workspaceId } ,
660- query : { page, page_size : pageSize , reverse } ,
665+ query : { page, size : pageSize , reverse } ,
661666 } ,
662667 body : filters ,
663668 } ,
@@ -690,6 +695,109 @@ export function useQueryConclusions(
690695 } ) ;
691696}
692697
698+ /**
699+ * Discover the actual conclusion target peers available for one observer peer.
700+ *
701+ * Walks `conclusions/list` (paginated, `page`/`size`) with `filters.observer_id`
702+ * and collects the distinct `observed_id` values from real stored conclusions.
703+ * This is what powers the target picker: the user picks among targets that
704+ * actually exist instead of guessing peer ids into an empty self-scope.
705+ *
706+ * Returns `{ targets, complete }`: `targets` is a sorted list of `{ id, count }`
707+ * (count = number of stored rows for that target, i.e. provenance strength),
708+ * `complete` is false when the walk hit the page cap before listing every page
709+ * (targets may then be incomplete). Empty list = this observer holds no
710+ * conclusions at all.
711+ *
712+ * The knowledge panel previously self-scoped `observed` to the peer, which is
713+ * always empty for knowledge-holder peers (a peer that holds knowledge *about*
714+ * other peers holds nothing about itself).
715+ */
716+ export function useConclusionTargetPeers ( workspaceId : string , observerPeerId : string ) {
717+ return useQuery ( {
718+ queryKey : [ "conclusions-target-peers" , workspaceId , observerPeerId ] as const ,
719+ queryFn : async ( ) => {
720+ const counters = new Map < string , number > ( ) ;
721+ let page = 1 ;
722+ let pages = 1 ;
723+ let completed = false ;
724+ while ( page <= CONCLUSION_TARGET_MAX_PAGES ) {
725+ const { data, error } = await client . current . POST (
726+ "/v3/workspaces/{workspace_id}/conclusions/list" ,
727+ {
728+ params : {
729+ path : { workspace_id : workspaceId } ,
730+ query : { page, size : CONCLUSION_TARGET_PAGE_SIZE , reverse : false } ,
731+ } ,
732+ body : { filters : { observer_id : observerPeerId } } ,
733+ } ,
734+ ) ;
735+ if ( error ) err ( error ) ;
736+ const items = data . items ?? [ ] ;
737+ for ( const item of items ) {
738+ if ( item . observed_id ) {
739+ counters . set ( item . observed_id , ( counters . get ( item . observed_id ) ?? 0 ) + 1 ) ;
740+ }
741+ }
742+ pages = data . pages ?? 1 ;
743+ if ( page >= pages || items . length === 0 ) {
744+ completed = true ;
745+ break ;
746+ }
747+ page += 1 ;
748+ }
749+ return {
750+ targets : Array . from ( counters . entries ( ) )
751+ . map ( ( [ id , count ] ) => ( { id, count } ) )
752+ . sort ( ( a , b ) => b . count - a . count || a . id . localeCompare ( b . id ) ) ,
753+ complete : completed ,
754+ } ;
755+ } ,
756+ enabled : Boolean ( workspaceId ) && Boolean ( observerPeerId ) ,
757+ } ) ;
758+ }
759+
760+ /**
761+ * Semantic search over conclusions in the observer/observed scope of one peer.
762+ *
763+ * Unlike `useSearchPeer` (message search on the peer itself), this queries the
764+ * dedicated conclusions store with the peer as the *observer*, so it also finds
765+ * knowledge the peer holds *about other peers*.
766+ * The server requires both an observer and an observed peer; the observed target
767+ * MUST be resolved by the caller (target-picker / auto-selection in PeerDetail).
768+ * No implicit self-scope: self-scoping produced guaranteed-zero results.
769+ */
770+ export function useQueryPeerConclusions (
771+ workspaceId : string ,
772+ peerId : string ,
773+ query : string ,
774+ observedPeerId : string | null ,
775+ enabled = false ,
776+ ) {
777+ const filters : Record < string , unknown > = { observer : peerId , observed : observedPeerId ?? "" } ;
778+ return useQuery ( {
779+ queryKey : QK . conclusionsQuery ( workspaceId , query , { ...filters , scope : "peer" } ) ,
780+ queryFn : async ( ) => {
781+ const { data, error } = await client . current . POST (
782+ "/v3/workspaces/{workspace_id}/conclusions/query" ,
783+ {
784+ params : { path : { workspace_id : workspaceId } } ,
785+ body : { query, top_k : 25 , filters } ,
786+ } ,
787+ ) ;
788+ return data ?? err ( error ) ;
789+ } ,
790+ // Never fire with an empty target: the server requires both filters, and
791+ // an observed="" request is a guaranteed zero (or worse, an error).
792+ enabled :
793+ enabled &&
794+ Boolean ( workspaceId ) &&
795+ Boolean ( peerId ) &&
796+ Boolean ( query ) &&
797+ Boolean ( observedPeerId ) ,
798+ } ) ;
799+ }
800+
693801export function useCreateConclusion ( workspaceId : string ) {
694802 const qc = useQueryClient ( ) ;
695803 return useMutation ( {
@@ -761,7 +869,7 @@ export function useDreams(
761869 {
762870 params : {
763871 path : { workspace_id : workspaceId } ,
764- query : { page, page_size : pageSize , reverse : false } ,
872+ query : { page, size : pageSize , reverse : false } ,
765873 } ,
766874 body : filters ,
767875 } ,
0 commit comments