(res: http.ServerResponse, url: URL)
| 764 | } |
| 765 | |
| 766 | private async serveTaskSearch(res: http.ServerResponse, url: URL): Promise<void> { |
| 767 | const q = (url.searchParams.get("q") ?? "").trim(); |
| 768 | if (!q) { this.jsonResponse(res, { tasks: [], total: 0 }); return; } |
| 769 | |
| 770 | const owner = url.searchParams.get("owner") ?? undefined; |
| 771 | const maxResults = Math.min(50, Math.max(1, Number(url.searchParams.get("limit")) || 20)); |
| 772 | |
| 773 | const scoreMap = new Map<string, number>(); |
| 774 | |
| 775 | if (this.embedder) { |
| 776 | try { |
| 777 | const [queryVec] = await this.embedder.embed([q]); |
| 778 | const allEmb = this.store.getTaskEmbeddings(owner); |
| 779 | for (const { taskId, vector } of allEmb) { |
| 780 | let dot = 0, normA = 0, normB = 0; |
| 781 | for (let i = 0; i < queryVec.length && i < vector.length; i++) { |
| 782 | dot += queryVec[i] * vector[i]; |
| 783 | normA += queryVec[i] * queryVec[i]; |
| 784 | normB += vector[i] * vector[i]; |
| 785 | } |
| 786 | const sim = normA > 0 && normB > 0 ? dot / (Math.sqrt(normA) * Math.sqrt(normB)) : 0; |
| 787 | if (sim > 0.3) scoreMap.set(taskId, sim); |
| 788 | } |
| 789 | } catch { /* embedding unavailable, fall through to FTS */ } |
| 790 | } |
| 791 | |
| 792 | const ftsResults = this.store.taskFtsSearch(q, maxResults, owner); |
| 793 | for (const { taskId, score } of ftsResults) { |
| 794 | const existing = scoreMap.get(taskId) ?? 0; |
| 795 | scoreMap.set(taskId, Math.max(existing, score * 0.8)); |
| 796 | } |
| 797 | |
| 798 | const sorted = [...scoreMap.entries()] |
| 799 | .sort((a, b) => b[1] - a[1]) |
| 800 | .slice(0, maxResults); |
| 801 | |
| 802 | const db = (this.store as any).db; |
| 803 | const tasks = sorted.map(([taskId, score]) => { |
| 804 | const t = this.store.getTask(taskId); |
| 805 | if (!t) return null; |
| 806 | const meta = db.prepare("SELECT skill_status, owner FROM tasks WHERE id = ?").get(taskId) as { skill_status: string | null; owner: string | null } | undefined; |
| 807 | const hubTask = this.getHubTaskForLocal(taskId); |
| 808 | const ts = this.resolveTaskTeamShareForApi(taskId, hubTask); |
| 809 | return { |
| 810 | id: t.id, sessionKey: t.sessionKey, title: t.title, |
| 811 | summary: t.summary ?? "", status: t.status, |
| 812 | startedAt: t.startedAt, endedAt: t.endedAt, |
| 813 | chunkCount: this.store.countChunksByTask(t.id), |
| 814 | skillStatus: meta?.skill_status ?? null, |
| 815 | owner: meta?.owner ?? "agent:main", |
| 816 | sharingVisibility: ts.visibility, |
| 817 | score, |
| 818 | }; |
| 819 | }).filter(Boolean); |
| 820 | |
| 821 | this.jsonResponse(res, { tasks, total: tasks.length }); |
| 822 | } |
| 823 |
no test coverage detected