Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/web/src/api/compareQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function useScopedWorkspaces(instance: Instance, page = 1, pageSize = 20)
queryFn: async () => {
const client = createScopedClient(instance);
const { data, error } = await client.POST("/v3/workspaces/list", {
params: { query: { page, page_size: pageSize } },
params: { query: { page, size: pageSize } },
body: {},
});
return data ?? err(error);
Expand All @@ -41,7 +41,7 @@ export function useScopedPeers(instance: Instance, workspaceId: string, page = 1
queryFn: async () => {
const client = createScopedClient(instance);
const { data, error } = await client.POST("/v3/workspaces/{workspace_id}/peers/list", {
params: { path: { workspace_id: workspaceId }, query: { page, page_size: pageSize } },
params: { path: { workspace_id: workspaceId }, query: { page, size: pageSize } },
body: {},
});
return data ?? err(error);
Expand Down
122 changes: 115 additions & 7 deletions packages/web/src/api/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function useWorkspaces(page = 1, pageSize = 20) {
queryKey: QK.workspaces(page, pageSize),
queryFn: async () => {
const { data, error } = await client.current.POST("/v3/workspaces/list", {
params: { query: { page, page_size: pageSize } },
params: { query: { page, size: pageSize } },
body: {},
});
return data ?? err(error);
Expand Down Expand Up @@ -139,7 +139,7 @@ export function usePeers(workspaceId: string, page = 1, pageSize = 20) {
const { data, error } = await client.current.POST(
"/v3/workspaces/{workspace_id}/peers/list",
{
params: { path: { workspace_id: workspaceId }, query: { page, page_size: pageSize } },
params: { path: { workspace_id: workspaceId }, query: { page, size: pageSize } },
body: {},
},
);
Expand Down Expand Up @@ -253,7 +253,7 @@ export function usePeerSessions(workspaceId: string, peerId: string, page = 1, p
{
params: {
path: { workspace_id: workspaceId, peer_id: peerId },
query: { page, page_size: pageSize },
query: { page, size: pageSize },
},
body: {},
},
Expand Down Expand Up @@ -323,7 +323,7 @@ export function useSessions(workspaceId: string, page = 1, pageSize = 20) {
{
params: {
path: { workspace_id: workspaceId },
query: { page, page_size: pageSize },
query: { page, size: pageSize },
},
body: {},
},
Expand Down Expand Up @@ -412,7 +412,7 @@ export function useSessionMessages(
{
params: {
path: { workspace_id: workspaceId, session_id: sessionId },
query: { page, page_size: pageSize },
query: { page, size: pageSize },
},
body: {},
},
Expand Down Expand Up @@ -627,6 +627,11 @@ export function useSessionContext(workspaceId: string, sessionId: string) {

// ─── Conclusions ──────────────────────────────────────────────────────────────

// Target-discovery walk bounds (see useConclusionTargetPeers): enough for
// realistically sized per-observer stores; discovery is a read-only listing.
const CONCLUSION_TARGET_PAGE_SIZE = 100;
const CONCLUSION_TARGET_MAX_PAGES = 40;

export function useConclusions(
workspaceId: string,
filters: Record<string, unknown> = {},
Expand All @@ -642,7 +647,7 @@ export function useConclusions(
{
params: {
path: { workspace_id: workspaceId },
query: { page, page_size: pageSize, reverse },
query: { page, size: pageSize, reverse },
},
body: filters,
},
Expand Down Expand Up @@ -675,6 +680,109 @@ export function useQueryConclusions(
});
}

/**
* Discover the actual conclusion target peers available for one observer peer.
*
* Walks `conclusions/list` (paginated, `page`/`size`) with `filters.observer_id`
* and collects the distinct `observed_id` values from real stored conclusions.
* This is what powers the target picker: the user picks among targets that
* actually exist instead of guessing peer ids into an empty self-scope.
*
* Returns `{ targets, complete }`: `targets` is a sorted list of `{ id, count }`
* (count = number of stored rows for that target, i.e. provenance strength),
* `complete` is false when the walk hit the page cap before listing every page
* (targets may then be incomplete). Empty list = this observer holds no
* conclusions at all.
*
* The knowledge panel previously self-scoped `observed` to the peer, which is
* always empty for knowledge-holder peers (a peer that holds knowledge *about*
* other peers holds nothing about itself).
*/
export function useConclusionTargetPeers(workspaceId: string, observerPeerId: string) {
return useQuery({
queryKey: ["conclusions-target-peers", workspaceId, observerPeerId] as const,
queryFn: async () => {
const counters = new Map<string, number>();
let page = 1;
let pages = 1;
let completed = false;
while (page <= CONCLUSION_TARGET_MAX_PAGES) {
const { data, error } = await client.current.POST(
"/v3/workspaces/{workspace_id}/conclusions/list",
{
params: {
path: { workspace_id: workspaceId },
query: { page, size: CONCLUSION_TARGET_PAGE_SIZE, reverse: false },
},
body: { filters: { observer_id: observerPeerId } },
},
);
if (error) err(error);
const items = data.items ?? [];
for (const item of items) {
if (item.observed_id) {
counters.set(item.observed_id, (counters.get(item.observed_id) ?? 0) + 1);
}
}
pages = data.pages ?? 1;
if (page >= pages || items.length === 0) {
completed = true;
break;
}
page += 1;
}
return {
targets: Array.from(counters.entries())
.map(([id, count]) => ({ id, count }))
.sort((a, b) => b.count - a.count || a.id.localeCompare(b.id)),
complete: completed,
};
},
enabled: Boolean(workspaceId) && Boolean(observerPeerId),
});
}

/**
* Semantic search over conclusions in the observer/observed scope of one peer.
*
* Unlike `useSearchPeer` (message search on the peer itself), this queries the
* dedicated conclusions store with the peer as the *observer*, so it also finds
* knowledge the peer holds *about other peers*.
* The server requires both an observer and an observed peer; the observed target
* MUST be resolved by the caller (target-picker / auto-selection in PeerDetail).
* No implicit self-scope: self-scoping produced guaranteed-zero results.
*/
export function useQueryPeerConclusions(
workspaceId: string,
peerId: string,
query: string,
observedPeerId: string | null,
enabled = false,
) {
const filters: Record<string, unknown> = { observer: peerId, observed: observedPeerId ?? "" };
return useQuery({
queryKey: QK.conclusionsQuery(workspaceId, query, { ...filters, scope: "peer" }),
queryFn: async () => {
const { data, error } = await client.current.POST(
"/v3/workspaces/{workspace_id}/conclusions/query",
{
params: { path: { workspace_id: workspaceId } },
body: { query, top_k: 25, filters },
},
);
return data ?? err(error);
},
// Never fire with an empty target: the server requires both filters, and
// an observed="" request is a guaranteed zero (or worse, an error).
enabled:
enabled &&
Boolean(workspaceId) &&
Boolean(peerId) &&
Boolean(query) &&
Boolean(observedPeerId),
});
}

export function useCreateConclusion(workspaceId: string) {
const qc = useQueryClient();
return useMutation({
Expand Down Expand Up @@ -746,7 +854,7 @@ export function useDreams(
{
params: {
path: { workspace_id: workspaceId },
query: { page, page_size: pageSize, reverse: false },
query: { page, size: pageSize, reverse: false },
},
body: filters,
},
Expand Down
Loading