Skip to content

Insert links to knowledge base from admin chat, and use vector search#29

Merged
djanogly merged 5 commits intodevfrom
openspec/article-link-widget-integration
Mar 14, 2026
Merged

Insert links to knowledge base from admin chat, and use vector search#29
djanogly merged 5 commits intodevfrom
openspec/article-link-widget-integration

Conversation

@djanogly
Copy link
Contributor

This pull request introduces improvements to the knowledge search experience in the inbox, updates how knowledge links are inserted, and refines the widget's message interaction and UI styling. The most significant changes are grouped below:

Knowledge Search and Insertion Improvements:

  • Switched the knowledge search from a query to an action (knowledge:searchWithEmbeddings), enabling more dynamic and responsive search results in useInboxConvex and updating the hook to manage knowledge search state with useEffect and useState. ([[1]](https://github.com/opencom-org/opencom/pull/29/files#diff-8c41c9000ef8f232062798bd1c7a03e6f82c2b876341179c290b4b99e7524935L147-R149), [[2]](https://github.com/opencom-org/opencom/pull/29/files#diff-8c41c9000ef8f232062798bd1c7a03e6f82c2b876341179c290b4b99e7524935R181-R211), [[3]](https://github.com/opencom-org/opencom/pull/29/files#diff-8c41c9000ef8f232062798bd1c7a03e6f82c2b876341179c290b4b99e7524935L191-R225), [[4]](https://github.com/opencom-org/opencom/pull/29/files#diff-8c41c9000ef8f232062798bd1c7a03e6f82c2b876341179c290b4b99e7524935R3))
  • Updated the insertion logic for knowledge articles so that links are now inserted in the format [title](article:id) for both article and internalArticle types, ensuring consistent referencing. ([apps/web/src/app/inbox/page.tsxL384-R388](https://github.com/opencom-org/opencom/pull/29/files#diff-f05cfd8fbff7ba91780a2726674e49ff587e720d6fc118df8ff30d3ef1eaac90L384-R388))
  • Modified the UI in InboxThreadPane to focus on inserting article links instead of full content, and adjusted the button behavior accordingly. ([apps/web/src/app/inbox/InboxThreadPane.tsxL210-R228](https://github.com/opencom-org/opencom/pull/29/files#diff-83253b7f536695a1c89d914afbc128313d6f6d850df460f17453f60cc920f181L210-R228))

Widget Message Interaction:

  • Added click handling to messages in the widget so that clicking on an article link (with data-article-id) triggers the article selection callback, improving navigation and interactivity. ([[1]](https://github.com/opencom-org/opencom/pull/29/files#diff-ce3c188b3ef7a87f8c7abba79a5aeb9a3a08315a5e68d668b530daf606e6ca10R74-R86), [[2]](https://github.com/opencom-org/opencom/pull/29/files#diff-ce3c188b3ef7a87f8c7abba79a5aeb9a3a08315a5e68d668b530daf606e6ca10R146))

UI and Styling Enhancements:

  • Reformatted and expanded CSS transitions and gradients throughout the widget's styles for better readability and maintainability, without changing the visual behavior. ([[1]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L37-R43), [[2]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L77-R86), [[3]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L156-R167), [[4]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L254-R267), [[5]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L344-R361), [[6]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L492-R512), [[7]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L518-R551), [[8]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L541-R569), [[9]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L802-R834), [[10]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L843-R876), [[11]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L936-R982), [[12]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L966-R1007), [[13]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L990-R1033), [[14]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L1200-R1245), [[15]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L1250-R1297), [[16]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L1326-R1375), [[17]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L1401-R1452), [[18]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L1685-R1739), [[19]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L1804-R1860))

Minor Code Quality and Formatting:

  • Code formatting improvements for readability in various files, including callback definitions and message rendering. ([[1]](https://github.com/opencom-org/opencom/pull/29/files#diff-f05cfd8fbff7ba91780a2726674e49ff587e720d6fc118df8ff30d3ef1eaac90L286-R297), [[2]](https://github.com/opencom-org/opencom/pull/29/files#diff-ce3c188b3ef7a87f8c7abba79a5aeb9a3a08315a5e68d668b530daf606e6ca10L105-R120), [[3]](https://github.com/opencom-org/opencom/pull/29/files#diff-ce3c188b3ef7a87f8c7abba79a5aeb9a3a08315a5e68d668b530daf606e6ca10L118-R136), [[4]](https://github.com/opencom-org/opencom/pull/29/files#diff-ce3c188b3ef7a87f8c7abba79a5aeb9a3a08315a5e68d668b530daf606e6ca10L153-R174))

These changes collectively enhance the user and developer experience around knowledge search, insertion, and message interaction in the inbox and widget.

@djanogly djanogly requested a review from Copilot March 13, 2026 21:26
@vercel
Copy link

vercel bot commented Mar 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
opencom-landing Ready Ready Preview, Comment Mar 13, 2026 11:44pm
opencom-web Ready Ready Preview, Comment Mar 13, 2026 11:44pm

@qodo-code-review
Copy link

Review Summary by Qodo

Integrate vector search for inbox knowledge and add article link widget support

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Implement vector search for inbox knowledge picker using embeddings
• Change article link format to article:<id> for widget integration
• Add article link detection in markdown with data-article-id attribute
• Enable widget to handle article links as in-widget navigation
Diagram
flowchart LR
  A["Inbox Knowledge Search"] -->|"uses vector embeddings"| B["searchWithEmbeddings Action"]
  B -->|"returns results"| C["useInboxConvex Hook"]
  C -->|"displays in picker"| D["InboxThreadPane"]
  D -->|"inserts as article:id"| E["Message Content"]
  E -->|"parsed by shared markdown"| F["data-article-id attribute"]
  F -->|"click handler"| G["Widget onSelectArticle"]
  G -->|"opens article view"| H["Widget Help Center"]
Loading

Grey Divider

File Changes

1. packages/convex/convex/knowledge.ts ✨ Enhancement +140/-3

Add vector search action for knowledge embeddings

packages/convex/convex/knowledge.ts


2. apps/web/src/app/inbox/hooks/useInboxConvex.ts ✨ Enhancement +36/-11

Switch knowledge search to vector action with useEffect

apps/web/src/app/inbox/hooks/useInboxConvex.ts


3. apps/web/src/app/inbox/page.tsx ✨ Enhancement +14/-11

Update article link format to article:id scheme

apps/web/src/app/inbox/page.tsx


View more (17)
4. apps/web/src/app/inbox/InboxThreadPane.tsx ✨ Enhancement +13/-5

Focus on link insertion, comment out content insertion

apps/web/src/app/inbox/InboxThreadPane.tsx


5. packages/web-shared/src/markdown.ts ✨ Enhancement +39/-11

Detect article links and emit data-article-id attribute

packages/web-shared/src/markdown.ts


6. packages/web-shared/src/markdown.test.ts 🧪 Tests +19/-0

Add tests for article link detection and rendering

packages/web-shared/src/markdown.test.ts


7. apps/widget/src/components/conversationView/MessageList.tsx ✨ Enhancement +24/-3

Add click handler for article link navigation

apps/widget/src/components/conversationView/MessageList.tsx


8. apps/widget/src/styles.css ✨ Enhancement +184/-47

Add article link styling and reformat CSS transitions

apps/widget/src/styles.css


9. openspec/changes/archive/2026-03-13-article-link-widget-integration/.openspec.yaml 📝 Documentation +2/-0

Create openspec change archive metadata file

openspec/changes/archive/2026-03-13-article-link-widget-integration/.openspec.yaml


10. openspec/changes/archive/2026-03-13-article-link-widget-integration/design.md 📝 Documentation +79/-0

Document design decisions for article link integration

openspec/changes/archive/2026-03-13-article-link-widget-integration/design.md


11. openspec/changes/archive/2026-03-13-article-link-widget-integration/proposal.md 📝 Documentation +30/-0

Document proposal for article link widget integration

openspec/changes/archive/2026-03-13-article-link-widget-integration/proposal.md


12. openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/ai-help-center-linked-sources/spec.md 📝 Documentation +11/-0

Add spec for consistent article link format

openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/ai-help-center-linked-sources/spec.md


13. openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/inbox-knowledge-insertion/spec.md 📝 Documentation +24/-0

Add spec for article link insertion behavior

openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/inbox-knowledge-insertion/spec.md


14. openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/inbox-knowledge-vector-search/spec.md 📝 Documentation +27/-0

Add spec for vector search in inbox knowledge

openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/inbox-knowledge-vector-search/spec.md


15. openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/shared-markdown-rendering-sanitization/spec.md 📝 Documentation +44/-0

Add spec for article link markdown detection

openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/shared-markdown-rendering-sanitization/spec.md


16. openspec/changes/archive/2026-03-13-article-link-widget-integration/tasks.md 📝 Documentation +46/-0

Document implementation tasks and checklist

openspec/changes/archive/2026-03-13-article-link-widget-integration/tasks.md


17. openspec/specs/ai-help-center-linked-sources/spec.md 📝 Documentation +11/-0

Add article link format consistency requirement

openspec/specs/ai-help-center-linked-sources/spec.md


18. openspec/specs/inbox-knowledge-insertion/spec.md 📝 Documentation +23/-5

Update article insertion scenarios with new format

openspec/specs/inbox-knowledge-insertion/spec.md


19. openspec/specs/inbox-knowledge-vector-search/spec.md 📝 Documentation +33/-0

Create new spec for vector search capability

openspec/specs/inbox-knowledge-vector-search/spec.md


20. openspec/specs/shared-markdown-rendering-sanitization/spec.md 📝 Documentation +18/-0

Add article link detection requirements

openspec/specs/shared-markdown-rendering-sanitization/spec.md


Grey Divider

Qodo Logo

@qodo-code-review
Copy link

qodo-code-review bot commented Mar 13, 2026

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (2) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Manual suggestions:* function refs📘 Rule violation ✓ Correctness
Description
New backend code creates makeFunctionReference("suggestions:getEmbeddingById") and
makeFunctionReference("suggestions:getContentById") instead of using generated internal
references. This weakens ref boundary consistency and bypasses the preferred generated-ref
mechanism.
Code

packages/convex/convex/knowledge.ts[R246-269]

+const GET_EMBEDDING_BY_ID_REF: EmbeddingQueryRef<
+  { id: Id<"contentEmbeddings"> },
+  Doc<"contentEmbeddings"> | null
+> = makeFunctionReference<
+  "query",
+  { id: Id<"contentEmbeddings"> },
+  Doc<"contentEmbeddings"> | null
+>("suggestions:getEmbeddingById");
+
+type ContentRecord = {
+  content: string;
+  title: string;
+  slug?: string;
+  tags?: string[];
+} | null;
+
+const GET_CONTENT_BY_ID_REF: EmbeddingQueryRef<
+  { contentType: KnowledgeContentType; contentId: string },
+  ContentRecord
+> = makeFunctionReference<
+  "query",
+  { contentType: KnowledgeContentType; contentId: string },
+  ContentRecord
+>("suggestions:getContentById");
Evidence
The compliance rules require using generated api/internal references over manual
makeFunctionReference strings when calling other Convex functions. The new code adds manual
string-based refs to suggestions:*, and the suggestions module exports these functions as
internal queries, implying a generated internal.suggestions.* ref should be used instead.

Rule 72008: Prefer generated backend function references over manual makeFunctionReference
Rule 96849: Standardize Convex backend function reference boundaries ordering
packages/convex/convex/knowledge.ts[246-269]
packages/convex/convex/suggestions.ts[573-626]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`packages/convex/convex/knowledge.ts` introduces manual string-based Convex refs (`makeFunctionReference(&amp;amp;amp;quot;suggestions:getEmbeddingById&amp;amp;amp;quot;)`, `makeFunctionReference(&amp;amp;amp;quot;suggestions:getContentById&amp;amp;amp;quot;)`). Compliance requires using generated `api`/`internal` references when available (e.g., `internal.suggestions.getEmbeddingById`).
## Issue Context
`suggestions.ts` exports `getEmbeddingById` and `getContentById` as `internalQuery`, so a generated `internal.suggestions.*` ref should exist and should be preferred over manual `makeFunctionReference`.
## Fix Focus Areas
- packages/convex/convex/knowledge.ts[239-269]
- packages/convex/convex/knowledge.ts[319-329]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. getShallowRunQuery chained cast 📘 Rule violation ✓ Correctness
Description
The new helper getShallowRunQuery uses a chained escape cast (as unknown as ...) without any
justification comment. This expands unsafe type-escape usage in runtime code beyond the allowed
documented exceptions.
Code

packages/convex/convex/knowledge.ts[R271-276]

+function getShallowRunQuery(ctx: { runQuery: unknown }) {
+  return ctx.runQuery as unknown as <Args extends Record<string, unknown>, Return>(
+    queryRef: EmbeddingQueryRef<Args, Return>,
+    queryArgs: Args
+  ) => Promise<Return>;
+}
Evidence
The compliance rule restricts unsafe type-escape casts like as unknown as unless they are tightly
scoped and documented with a brief comment explaining why the type system cannot represent the
pattern. The newly added helper uses such a cast with no documentation.

Rule 72009: Restrict unsafe type-escape casts in new runtime code
packages/convex/convex/knowledge.ts[271-276]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`getShallowRunQuery` introduces a chained type-escape cast (`as unknown as ...`) with no documentation. The rule requires these casts to be minimal, hotspot-local, and documented.
## Issue Context
This helper is used to call `ctx.runQuery(...)` inside `searchWithEmbeddings`. Ideally, type `ctx` using the correct Convex action context type (or a properly typed `runQuery` signature) to eliminate the cast; if a cast is unavoidable, add a `NOTE:` comment explaining why and how it can be removed.
## Fix Focus Areas
- packages/convex/convex/knowledge.ts[271-276]
- packages/convex/convex/knowledge.ts[282-299]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Article content insert removed🐞 Bug ✓ Correctness
Description
InboxThreadPane no longer provides an “Insert Content” action for article results, so agents cannot
insert full article text from the picker. This violates the inbox knowledge insertion spec and
regresses existing behavior.
Code

apps/web/src/app/inbox/InboxThreadPane.tsx[R210-212]

+          {/* <Button size="sm" variant="outline" onClick={() => onInsertKnowledgeContent(item)}>
         Insert Content
-          </Button>
+          </Button> */}
Evidence
The spec explicitly requires both link insertion and content insertion for articles, but the UI now
comments out the content insertion control and only exposes link insertion for articles.

openspec/specs/inbox-knowledge-insertion/spec.md[25-46]
apps/web/src/app/inbox/InboxThreadPane.tsx[199-229]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Inbox knowledge picker no longer offers an &amp;amp;amp;quot;Insert Content&amp;amp;amp;quot; action for articles, violating the spec and regressing agent workflows.
### Issue Context
The UI currently only exposes &amp;amp;amp;quot;Insert Link&amp;amp;amp;quot; for articles (and even comments out the existing content button). The spec requires agents to be able to choose between inserting a link or inserting the article’s content.
### Fix Focus Areas
- apps/web/src/app/inbox/InboxThreadPane.tsx[190-229]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Internal article link broken🐞 Bug ✓ Correctness
Description
handleInsertKnowledgeContent now inserts [title](article:) for internalArticle items, but the
widget resolves selected IDs via articles:getForVisitor (public/published articles only).
Internal article links will be clickable yet resolve to “unavailable” (or be skipped) for visitors.
Code

apps/web/src/app/inbox/page.tsx[R387-388]

+    } else if (action === "link" && (item.type === "article" || item.type === "internalArticle")) {
+      setInputValue((prev) => `${prev}${prev ? "\n\n" : ""}[${item.title}](article:${item.id})`);
Evidence
The web inbox now inserts article: links for internal articles, while the widget only fetches
visitor-readable content from the articles table (and rejects anything not visitor-readable).
Additionally, internal-article content IDs can be legacy internalArticles IDs, which are not the
same resource as visitor articles.

apps/web/src/app/inbox/page.tsx[371-391]
apps/widget/src/Widget.tsx[519-535]
packages/convex/convex/articles.ts[1146-1166]
packages/convex/convex/suggestions.ts[633-647]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Internal articles are currently inserted as `article:&amp;amp;amp;lt;id&amp;amp;amp;gt;` links, but the widget’s visitor article resolver only supports visitor-readable `articles`. This makes internal-article links effectively broken for visitors.
### Issue Context
Widget fetches selected articles via `articles:getForVisitor`, which rejects non-visitor-readable content. Some internal content may also be stored under legacy `internalArticles` IDs.
### Fix Focus Areas
- apps/web/src/app/inbox/page.tsx[371-391]
- apps/web/src/app/inbox/InboxThreadPane.tsx[190-229]
- packages/convex/convex/articles.ts[1146-1166]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. User-injectable article navigation🐞 Bug ⛨ Security
Description
The shared markdown sanitizer allows data-article-id on sanitized HTML, and the widget treats any
clicked element with that attribute as an article link. Because raw HTML is enabled in markdown, a
user can inject data-article-id into arbitrary elements and cause unexpected in-widget navigation
on click.
Code

packages/web-shared/src/markdown.ts[34]

+const ALLOWED_ATTR = ["href", "target", "rel", "src", "alt", "title", "class", "data-article-id"];
Evidence
Markdown rendering accepts raw HTML (html: true). With data-article-id added to the global
DOMPurify allowlist, any message author can include that attribute on allowed tags. The widget click
handler uses attribute presence—not the article: protocol—to decide navigation.

packages/web-shared/src/markdown.ts[4-8]
packages/web-shared/src/markdown.ts[34-35]
apps/widget/src/components/ConversationView.tsx[437-447]
apps/widget/src/components/conversationView/MessageList.tsx[74-84]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`data-article-id` is currently globally allowlisted, making it possible to inject article-navigation triggers via raw HTML in markdown.
### Issue Context
The attribute is added post-sanitization for real `article:` links, so it does not need to be allowed on input. The widget uses `[data-article-id]` for navigation.
### Fix Focus Areas
- packages/web-shared/src/markdown.ts[10-146]
- apps/widget/src/components/conversationView/MessageList.tsx[74-86]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Article links not keyboardable🐞 Bug ✓ Correctness
Description
Article links have their href removed during markdown post-processing, which makes them
non-focusable and non-activatable via keyboard by default. Since the widget only handles mouse
clicks, article navigation becomes mouse-only.
Code

packages/web-shared/src/markdown.ts[R105-113]

+    if (isArticleLink(href)) {
+      const articleId = extractArticleId(href);
+      if (articleId) {
+        anchor.setAttribute("data-article-id", articleId);
+        anchor.setAttribute("class", "opencom-article-link");
+        anchor.removeAttribute("href");
+        anchor.removeAttribute("target");
+        anchor.removeAttribute("rel");
+      } else {
Evidence
The sanitizer explicitly removes href for article links, and the widget relies on a click handler
(no keyboard handling). Without an href or tabindex, anchors are typically not keyboard focus
targets.

packages/web-shared/src/markdown.ts[105-113]
apps/widget/src/components/conversationView/MessageList.tsx[141-147]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Article links are not focusable/keyboard-activatable because `href` is removed and only click handling is implemented.
### Issue Context
Widget delegates navigation via `[data-article-id]` click detection. To support keyboard users, the rendered element must be focusable and the widget must handle key activation.
### Fix Focus Areas
- packages/web-shared/src/markdown.ts[83-139]
- apps/widget/src/components/conversationView/MessageList.tsx[74-147]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Vector search query fanout🐞 Bug ➹ Performance
Description
knowledge:searchWithEmbeddings performs two runQuery calls per vector hit (embedding doc +
content) for limit * 8 hits before deduping, creating a large fanout per search. This increases
latency and backend load unnecessarily.
Code

packages/convex/convex/knowledge.ts[R304-344]

+    const results = await ctx.vectorSearch("contentEmbeddings", "by_embedding", {
+      vector: embedding,
+      limit: limit * 8,
+      filter: (q) => q.eq("workspaceId", args.workspaceId),
+    });
+
+    const contentTypeSet =
+      args.contentTypes && args.contentTypes.length > 0 ? new Set(args.contentTypes) : null;
+
+    const enrichedResults: (KnowledgeSearchResult | null)[] = await Promise.all(
+      results.map(
+        async (result: {
+          _id: Id<"contentEmbeddings">;
+          _score: number;
+        }): Promise<KnowledgeSearchResult | null> => {
+          const doc = await runQuery(GET_EMBEDDING_BY_ID_REF, {
+            id: result._id,
+          });
+          if (!doc) return null;
+          if (contentTypeSet && !contentTypeSet.has(doc.contentType)) return null;
+
+          const contentRecord = await runQuery(GET_CONTENT_BY_ID_REF, {
+            contentType: doc.contentType,
+            contentId: doc.contentId,
+          });
+          if (!contentRecord) return null;
+
+          return {
+            id: doc.contentId,
+            type: doc.contentType,
+            title: doc.title,
+            content: contentRecord.content,
+            snippet: doc.snippet,
+            slug: contentRecord.slug,
+            tags: contentRecord.tags,
+            relevanceScore: result._score,
+            updatedAt: doc.updatedAt,
+          };
+        }
+      )
+    );
Evidence
The implementation fetches limit*8 vector results, then for each result performs separate queries
for embedding and content, only later filtering/deduping down to the requested limit.

packages/convex/convex/knowledge.ts[304-344]
packages/convex/convex/knowledge.ts[346-359]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The embedding search action does per-hit lookups before deduping, causing unnecessary query fanout.
### Issue Context
A similar vector-search flow exists in `aiAgentActionsKnowledge.ts` that dedupes before fetching full content.
### Fix Focus Areas
- packages/convex/convex/knowledge.ts[299-362]
- packages/convex/convex/aiAgentActionsKnowledge.ts[86-157]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
8. No embedding query limits🐞 Bug ➹ Performance
Description
knowledge:searchWithEmbeddings embeds arbitrary args.query without enforcing minimum/maximum
length, and the web inbox triggers it on every input change without debouncing. This can drive
unnecessary embedding cost and backend load under fast typing or long inputs.
Code

apps/web/src/app/inbox/hooks/useInboxConvex.ts[R186-205]

+  useEffect(() => {
+    if (!workspaceId || knowledgeSearch.trim().length < 1) {
+      setKnowledgeResults(undefined);
+      return;
+    }
+
+    let cancelled = false;
+
+    searchKnowledge({ workspaceId, query: knowledgeSearch, limit: 20 })
+      .then((results) => {
+        if (!cancelled) {
+          setKnowledgeResults(results);
+        }
+      })
+      .catch((error) => {
+        console.error("Knowledge search failed:", error);
+        if (!cancelled) {
+          setKnowledgeResults(undefined);
+        }
+      });
Evidence
The action always calls embed on the raw query string, and the client useEffect fires per
keystroke; there are no length guards like other embedding-backed endpoints already have.

packages/convex/convex/knowledge.ts[299-303]
apps/web/src/app/inbox/hooks/useInboxConvex.ts[186-205]
packages/convex/convex/suggestions.ts[669-677]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Embedding generation is invoked for unbounded query strings and is triggered on each input change, increasing cost and load.
### Issue Context
`suggestions:searchForWidget` already enforces min/max query length. Inbox knowledge search should apply similar constraints and reduce call frequency.
### Fix Focus Areas
- packages/convex/convex/knowledge.ts[282-363]
- apps/web/src/app/inbox/hooks/useInboxConvex.ts[181-210]
- packages/convex/convex/suggestions.ts[659-677]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@qodo-code-review
Copy link

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: checks

Failed stage: Summarize check results [❌]

Failed test name: ""

Failure summary:

The GitHub Action failed at the dependency vulnerability audit step (the “Dependency audit gate”).
-
The audit reported multiple high severity advisories for the package undici (e.g.
GHSA-f269-vfmq-vjvj, GHSA-vrm6-8vpv-qv8q, GHSA-v9p9-hfj2-hcw8).
- Each advisory was flagged as
reason: not allowlisted, so the gate treated them as blocking and exited non-zero (Process completed
with exit code 1).
- Earlier lint produced only warnings and tests/typecheck/build all passed; the
failure was specifically the dependency audit gate.

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

206:  packages/types lint: Warning: React version was set to "detect" in eslint-plugin-react settings, but the "react" package is not installed. Assuming latest React version for linting.
207:  packages/ui lint: Done
208:  packages/types lint: Done
209:  apps/landing lint$ next lint
210:  packages/convex lint$ eslint convex scripts tests --ext .ts
211:  apps/landing lint: `next lint` is deprecated and will be removed in Next.js 16.
212:  apps/landing lint: For new projects, use create-next-app to choose your preferred linter.
213:  apps/landing lint: For existing projects, migrate to the ESLint CLI:
214:  apps/landing lint: npx @next/codemod@canary next-lint-to-eslint-cli .
215:  packages/convex lint: Warning: React version was set to "detect" in eslint-plugin-react settings, but the "react" package is not installed. Assuming latest React version for linting.
216:  apps/landing lint:  ⚠ The Next.js plugin was not detected in your ESLint configuration. See https://nextjs.org/docs/app/api-reference/config/eslint#migrating-existing-config
217:  apps/landing lint: Attention: Next.js now collects completely anonymous telemetry regarding usage.
218:  apps/landing lint: This information is used to shape Next.js' roadmap and prioritize features.
219:  apps/landing lint: You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
220:  apps/landing lint: https://nextjs.org/telemetry
221:  apps/landing lint: ✔ No ESLint warnings or errors
222:  apps/landing lint: Done
...

377:  packages/convex lint:   90:12  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
378:  packages/convex lint: /home/runner/work/opencom/opencom/packages/convex/tests/visitorDirectoryAuthorizationSemantics.test.ts
379:  packages/convex lint:    62:81  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
380:  packages/convex lint:    74:10  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
381:  packages/convex lint:    77:81  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
382:  packages/convex lint:    91:10  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
383:  packages/convex lint:   116:13  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
384:  packages/convex lint:   138:10  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
385:  packages/convex lint:   165:13  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
386:  packages/convex lint:   187:10  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
387:  packages/convex lint:   200:65  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
388:  packages/convex lint:   215:10  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
389:  packages/convex lint:   233:65  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
390:  packages/convex lint:   254:10  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
391:  packages/convex lint:   348:65  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
392:  packages/convex lint: ✖ 146 problems (0 errors, 146 warnings)
393:  packages/convex lint: Done
394:  apps/mobile lint$ eslint . --ext .ts,.tsx
395:  apps/web lint$ eslint src --ext .ts,.tsx
396:  apps/mobile lint: /home/runner/work/opencom/opencom/apps/mobile/src/contexts/BackendContext.tsx
397:  apps/mobile lint:   33:6  warning  React Hook useEffect has a missing dependency: 'loadBackendStorage'. Either include it or remove the dependency array  react-hooks/exhaustive-deps
398:  apps/mobile lint: ✖ 1 problem (0 errors, 1 warning)
399:  apps/mobile lint: Done
400:  apps/web lint: /home/runner/work/opencom/opencom/apps/web/src/app/login/page.test.tsx
401:  apps/web lint:   45:42  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
402:  apps/web lint: /home/runner/work/opencom/opencom/apps/web/src/app/settings/MessengerSettingsSection.test.tsx
403:  apps/web lint:   24:61  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
404:  apps/web lint: /home/runner/work/opencom/opencom/apps/web/src/app/signup/page.test.tsx
405:  apps/web lint:   42:42  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
406:  apps/web lint: /home/runner/work/opencom/opencom/apps/web/src/components/ResponsiveLayout.tsx
407:  apps/web lint:   152:6  warning  React Hook useEffect has a missing dependency: 'closePanel'. Either include it or remove the dependency array  react-hooks/exhaustive-deps
408:  apps/web lint: /home/runner/work/opencom/opencom/apps/web/src/contexts/BackendContext.tsx
409:  apps/web lint:   86:6  warning  React Hook useEffect has a missing dependency: 'loadBackendStorage'. Either include it or remove the dependency array  react-hooks/exhaustive-deps
410:  apps/web lint: ✖ 5 problems (0 errors, 5 warnings)
411:  apps/web lint: Done
...

413:  apps/widget lint: /home/runner/work/opencom/opencom/apps/widget/src/components/ConversationView.test.tsx
414:  apps/widget lint:    53:6   warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
415:  apps/widget lint:    55:32  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
416:  apps/widget lint:    71:48  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
417:  apps/widget lint:    96:74  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
418:  apps/widget lint:   265:37  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
419:  apps/widget lint:   266:35  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
420:  apps/widget lint:   284:49  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
421:  apps/widget lint: /home/runner/work/opencom/opencom/apps/widget/src/components/ConversationView.tsx
422:  apps/widget lint:   162:6  warning  React Hook useEffect has missing dependencies: 'sessionTokenRef' and 'visitorId'. Either include them or remove the dependency array  react-hooks/exhaustive-deps
423:  apps/widget lint: /home/runner/work/opencom/opencom/apps/widget/src/components/HelpCenter.tsx
424:  apps/widget lint:   149:6  warning  React Hook useEffect has a missing dependency: 'setSelectedCollectionKey'. Either include it or remove the dependency array  react-hooks/exhaustive-deps
425:  apps/widget lint:   158:6  warning  React Hook useEffect has a missing dependency: 'setSelectedCollectionKey'. Either include it or remove the dependency array  react-hooks/exhaustive-deps
426:  apps/widget lint: /home/runner/work/opencom/opencom/apps/widget/src/hooks/useWidgetConversationFlow.ts
427:  apps/widget lint:   192:6  warning  React Hook useCallback has a missing dependency: 'visitorIdRef'. Either include it or remove the dependency array  react-hooks/exhaustive-deps
428:  apps/widget lint: ✖ 11 problems (0 errors, 11 warnings)
429:  apps/widget lint: Done
...

496:  > node scripts/ci-security-headers-check.js
497:  [security-headers-check] OK: web and landing header requirements validated.
498:  ##[group]Run pnpm test:convex
499:  �[36;1mpnpm test:convex�[0m
500:  shell: /usr/bin/bash -e {0}
501:  env:
502:  PNPM_HOME: /home/runner/setup-pnpm/node_modules/.bin
503:  ##[endgroup]
504:  > opencom@0.1.0 test:convex /home/runner/work/opencom/opencom
505:  > pnpm --filter @opencom/convex test
506:  > @opencom/convex@0.1.0 test /home/runner/work/opencom/opencom/packages/convex
507:  > vitest run
508:  �[1m�[46m RUN �[49m�[22m �[36mv4.0.17 �[39m�[90m/home/runner/work/opencom/opencom/packages/convex�[39m
509:  �[32m✓�[39m tests/runtimeTypeHardeningGuard.test.ts �[2m(�[22m�[2m25 tests�[22m�[2m)�[22m�[32m 51�[2mms�[22m�[39m
510:  �[90mstderr�[2m | tests/aiAgentRuntimeSafety.test.ts�[2m > �[22m�[2maiAgentActions runtime safety�[2m > �[22m�[2mclears prior diagnostics and continues generation when configuration is valid
511:  �[22m�[39mKnowledge retrieval failed; continuing without knowledge context: TypeError: runAction is not a function
512:  at Function.handler [as _handler] �[90m(/home/runner/work/opencom/opencom/packages/convex/�[39mconvex/aiAgentActions.ts:580:32�[90m)�[39m
513:  �[90m    at processTicksAndRejections (node:internal/process/task_queues:95:5)�[39m
514:  at �[90m/home/runner/work/opencom/opencom/packages/convex/�[39mtests/aiAgentRuntimeSafety.test.ts:252:20
515:  at file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/@vitest+runner@4.0.17/node_modules/�[4m@vitest/runner�[24m/dist/index.js:915:20
516:  �[90mstderr�[2m | tests/aiAgentRuntimeSafety.test.ts�[2m > �[22m�[2maiAgentActions runtime safety�[2m > �[22m�[2mretries once when first generation returns empty text and succeeds on retry
517:  �[22m�[39mKnowledge retrieval failed; continuing without knowledge context: TypeError: runAction is not a function
518:  at Function.handler [as _handler] �[90m(/home/runner/work/opencom/opencom/packages/convex/�[39mconvex/aiAgentActions.ts:580:32�[90m)�[39m
519:  �[90m    at processTicksAndRejections (node:internal/process/task_queues:95:5)�[39m
520:  at �[90m/home/runner/work/opencom/opencom/packages/convex/�[39mtests/aiAgentRuntimeSafety.test.ts:334:20
521:  at file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/@vitest+runner@4.0.17/node_modules/�[4m@vitest/runner�[24m/dist/index.js:915:20
522:  �[90mstderr�[2m | tests/aiAgentRuntimeSafety.test.ts�[2m > �[22m�[2maiAgentActions runtime safety�[2m > �[22m�[2momits temperature for gpt-5 reasoning models
523:  �[22m�[39mKnowledge retrieval failed; continuing without knowledge context: TypeError: runAction is not a function
524:  at Function.handler [as _handler] �[90m(/home/runner/work/opencom/opencom/packages/convex/�[39mconvex/aiAgentActions.ts:580:32�[90m)�[39m
525:  �[90m    at processTicksAndRejections (node:internal/process/task_queues:95:5)�[39m
526:  at �[90m/home/runner/work/opencom/opencom/packages/convex/�[39mtests/aiAgentRuntimeSafety.test.ts:410:5
527:  at file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/@vitest+runner@4.0.17/node_modules/�[4m@vitest/runner�[24m/dist/index.js:915:20
528:  �[90mstderr�[2m | tests/aiAgentRuntimeSafety.test.ts�[2m > �[22m�[2maiAgentActions runtime safety�[2m > �[22m�[2mstores the handoff message while retaining generated candidate context
529:  �[22m�[39mKnowledge retrieval failed; continuing without knowledge context: TypeError: runAction is not a function
530:  at Function.handler [as _handler] �[90m(/home/runner/work/opencom/opencom/packages/convex/�[39mconvex/aiAgentActions.ts:580:32�[90m)�[39m
531:  �[90m    at processTicksAndRejections (node:internal/process/task_queues:95:5)�[39m
532:  at �[90m/home/runner/work/opencom/opencom/packages/convex/�[39mtests/aiAgentRuntimeSafety.test.ts:482:20
533:  at file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/@vitest+runner@4.0.17/node_modules/�[4m@vitest/runner�[24m/dist/index.js:915:20
534:  �[90mstderr�[2m | tests/aiAgentRuntimeSafety.test.ts�[2m > �[22m�[2maiAgentActions runtime safety�[2m > �[22m�[2mdoes not hand off when a resolved answer includes an optional human escalation offer
535:  �[22m�[39mKnowledge retrieval failed; continuing without knowledge context: TypeError: runAction is not a function
536:  at Function.handler [as _handler] �[90m(/home/runner/work/opencom/opencom/packages/convex/�[39mconvex/aiAgentActions.ts:580:32�[90m)�[39m
537:  �[90m    at processTicksAndRejections (node:internal/process/task_queues:95:5)�[39m
538:  at �[90m/home/runner/work/opencom/opencom/packages/convex/�[39mtests/aiAgentRuntimeSafety.test.ts:562:20
539:  at file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/@vitest+runner@4.0.17/node_modules/�[4m@vitest/runner�[24m/dist/index.js:915:20
540:  �[90mstderr�[2m | tests/aiAgentRuntimeSafety.test.ts�[2m > �[22m�[2maiAgentActions runtime safety�[2m > �[22m�[2mpersists a handoff message when generation fails
541:  �[22m�[39mKnowledge retrieval failed; continuing without knowledge context: TypeError: runAction is not a function
542:  at Function.handler [as _handler] �[90m(/home/runner/work/opencom/opencom/packages/convex/�[39mconvex/aiAgentActions.ts:580:32�[90m)�[39m
543:  �[90m    at processTicksAndRejections (node:internal/process/task_queues:95:5)�[39m
544:  at �[90m/home/runner/work/opencom/opencom/packages/convex/�[39mtests/aiAgentRuntimeSafety.test.ts:636:20
545:  at file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/@vitest+runner@4.0.17/node_modules/�[4m@vitest/runner�[24m/dist/index.js:915:20
546:  �[90mstderr�[2m | tests/aiAgentRuntimeSafety.test.ts�[2m > �[22m�[2maiAgentActions runtime safety�[2m > �[22m�[2mpersists a handoff message when generation fails
547:  �[22m�[39mAI generation error: Error: gateway timeout
548:  at �[90m/home/runner/work/opencom/opencom/packages/convex/�[39mtests/aiAgentRuntimeSafety.test.ts:592:40
549:  at file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/@vitest+runner@4.0.17/node_modules/�[4m@vitest/runner�[24m/dist/index.js:145:11
550:  at file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/@vitest+runner@4.0.17/node_modules/�[4m@vitest/runner�[24m/dist/index.js:915:26
551:  at file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/@vitest+runner@4.0.17/node_modules/�[4m@vitest/runner�[24m/dist/index.js:1243:20
552:  at new Promise (<anonymous>)
553:  at runWithTimeout (file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/@vitest+runner@4.0.17/node_modules/�[4m@vitest/runner�[24m/dist/index.js:1209:10)
554:  at file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/@vitest+runner@4.0.17/node_modules/�[4m@vitest/runner�[24m/dist/index.js:1653:37
555:  at Traces.$ (file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/vitest@4.0.17_@opentelemetry+api@1.9.0_@types+node@20.19.30_@vitest+ui@4.0.18_jiti@1.21.7_jsd_znh4lc65ld7o7n72tbcfnzmloy/node_modules/�[4mvitest�[24m/dist/chunks/traces.CCmnQaNT.js:142:27)
556:  at trace (file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/vitest@4.0.17_@opentelemetry+api@1.9.0_@types+node@20.19.30_@vitest+ui@4.0.18_jiti@1.21.7_jsd_znh4lc65ld7o7n72tbcfnzmloy/node_modules/�[4mvitest�[24m/dist/chunks/test.B8ej_ZHS.js:239:21)
557:  at runTest (file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/@vitest+runner@4.0.17/node_modules/�[4m@vitest/runner�[24m/dist/index.js:1653:12)
558:  �[90mstderr�[2m | tests/aiAgentRuntimeSafety.test.ts�[2m > �[22m�[2maiAgentActions runtime safety�[2m > �[22m�[2mfalls back to a persisted bot message if handoff fails after generation error
559:  �[22m�[39mKnowledge retrieval failed; continuing without knowledge context: TypeError: runAction is not a function
560:  at Function.handler [as _handler] �[90m(/home/runner/work/opencom/opencom/packages/convex/�[39mconvex/aiAgentActions.ts:580:32�[90m)�[39m
561:  �[90m    at processTicksAndRejections (node:internal/process/task_queues:95:5)�[39m
562:  at �[90m/home/runner/work/opencom/opencom/packages/convex/�[39mtests/aiAgentRuntimeSafety.test.ts:711:20
563:  at file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/@vitest+runner@4.0.17/node_modules/�[4m@vitest/runner�[24m/dist/index.js:915:20
564:  �[90mstderr�[2m | tests/aiAgentRuntimeSafety.test.ts�[2m > �[22m�[2maiAgentActions runtime safety�[2m > �[22m�[2mfalls back to a persisted bot message if handoff fails after generation error
565:  �[22m�[39mAI generation error: Error: provider unavailable
566:  at �[90m/home/runner/work/opencom/opencom/packages/convex/�[39mtests/aiAgentRuntimeSafety.test.ts:670:40
567:  at file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/@vitest+runner@4.0.17/node_modules/�[4m@vitest/runner�[24m/dist/index.js:145:11
568:  at file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/@vitest+runner@4.0.17/node_modules/�[4m@vitest/runner�[24m/dist/index.js:915:26
569:  at file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/@vitest+runner@4.0.17/node_modules/�[4m@vitest/runner�[24m/dist/index.js:1243:20
570:  at new Promise (<anonymous>)
571:  at runWithTimeout (file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/@vitest+runner@4.0.17/node_modules/�[4m@vitest/runner�[24m/dist/index.js:1209:10)
572:  at file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/@vitest+runner@4.0.17/node_modules/�[4m@vitest/runner�[24m/dist/index.js:1653:37
573:  at Traces.$ (file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/vitest@4.0.17_@opentelemetry+api@1.9.0_@types+node@20.19.30_@vitest+ui@4.0.18_jiti@1.21.7_jsd_znh4lc65ld7o7n72tbcfnzmloy/node_modules/�[4mvitest�[24m/dist/chunks/traces.CCmnQaNT.js:142:27)
574:  at trace (file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/vitest@4.0.17_@opentelemetry+api@1.9.0_@types+node@20.19.30_@vitest+ui@4.0.18_jiti@1.21.7_jsd_znh4lc65ld7o7n72tbcfnzmloy/node_modules/�[4mvitest�[24m/dist/chunks/test.B8ej_ZHS.js:239:21)
575:  at runTest (file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/@vitest+runner@4.0.17/node_modules/�[4m@vitest/runner�[24m/dist/index.js:1653:12)
576:  �[90mstderr�[2m | tests/aiAgentRuntimeSafety.test.ts�[2m > �[22m�[2maiAgentActions runtime safety�[2m > �[22m�[2mfalls back to a persisted bot message if handoff fails after generation error
577:  �[22m�[39mFailed to handoff after AI generation error: Error: handoff unavailable
578:  at �[90m/home/runner/work/opencom/opencom/packages/convex/�[39mtests/aiAgentRuntimeSafety.test.ts:700:15
579:  at Mock (file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/@vitest+spy@4.0.17/node_modules/�[4m@vitest/spy�[24m/dist/index.js:285:34)
580:  at handleGenerationFailure �[90m(/home/runner/work/opencom/opencom/packages/convex/�[39mconvex/aiAgentActions.ts:631:31�[90m)�[39m
581:  �[90m    at processTicksAndRejections (node:internal/process/task_queues:95:5)�[39m
582:  at Function.handler [as _handler] �[90m(/home/runner/work/opencom/opencom/packages/convex/�[39mconvex/aiAgentActions.ts:802:14�[90m)�[39m
583:  at �[90m/home/runner/work/opencom/opencom/packages/convex/�[39mtests/aiAgentRuntimeSafety.test.ts:711:20
584:  at file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/@vitest+runner@4.0.17/node_modules/�[4m@vitest/runner�[24m/dist/index.js:915:20
585:  �[90mstderr�[2m | tests/aiAgentRuntimeSafety.test.ts�[2m > �[22m�[2maiAgentActions runtime safety�[2m > �[22m�[2mtreats empty model output as a generation failure and hands off
586:  �[22m�[39mKnowledge retrieval failed; continuing without knowledge context: TypeError: runAction is not a function
587:  at Function.handler [as _handler] �[90m(/home/runner/work/opencom/opencom/packages/convex/�[39mconvex/aiAgentActions.ts:580:32�[90m)�[39m
588:  �[90m    at processTicksAndRejections (node:internal/process/task_queues:95:5)�[39m
589:  at �[90m/home/runner/work/opencom/opencom/packages/convex/�[39mtests/aiAgentRuntimeSafety.test.ts:810:20
590:  at file:///home/runner/work/opencom/opencom/node_modules/�[4m.pnpm�[24m/@vitest+runner@4.0.17/node_modules/�[4m@vitest/runner�[24m/dist/index.js:915:20
591:  �[32m✓�[39m tests/aiAgentRuntimeSafety.test.ts �[2m(�[22m�[2m14 tests�[22m�[2m)�[22m�[32m 71�[2mms�[22m�[39m
592:  �[32m✓�[39m tests/reportingCsatEligibilitySemantics.test.ts �[2m(�[22m�[2m6 tests�[22m�[2m)�[22m�[32m 17�[2mms�[22m�[39m
593:  �[32m✓�[39m tests/visitorDirectoryAuthorizationSemantics.test.ts �[2m(�[22m�[2m7 tests�[22m�[2m)�[22m�[32m 17�[2mms�[22m�[39m
594:  �[90mstderr�[2m | tests/notificationRouting.test.ts�[2m > �[22m�[2mnotification routing�[2m > �[22m�[2mremoves invalid agent and visitor tokens after transport errors
595:  �[22m�[39m[Push] Failed to send to ExponentPushToken[agent-invalid]: DeviceNotRegistered: The device is not registered
596:  [Push] Failed to send to ExponentPushToken[visitor-invalid]: DeviceNotRegistered: The device is not registered
597:  �[32m✓�[39m tests/auditLogs.test.ts �[2m(�[22m�[2m17 tests�[22m�[2m)�[22m�[32m 15�[2mms�[22m�[39m
...

730:  - [high] undici (id=1114592): Undici: Malicious WebSocket 64-bit length overflows parser and crashes the client
731:  reason: not allowlisted
732:  advisory: https://github.com/advisories/GHSA-f269-vfmq-vjvj
733:  - [high] undici (id=1114637): Undici has Unbounded Memory Consumption in WebSocket permessage-deflate Decompression
734:  reason: not allowlisted
735:  advisory: https://github.com/advisories/GHSA-vrm6-8vpv-qv8q
736:  - [high] undici (id=1114638): Undici has Unbounded Memory Consumption in WebSocket permessage-deflate Decompression
737:  reason: not allowlisted
738:  advisory: https://github.com/advisories/GHSA-vrm6-8vpv-qv8q
739:  - [high] undici (id=1114639): Undici has Unhandled Exception in WebSocket Client Due to Invalid server_max_window_bits Validation
740:  reason: not allowlisted
741:  advisory: https://github.com/advisories/GHSA-v9p9-hfj2-hcw8
742:  - [high] undici (id=1114640): Undici has Unhandled Exception in WebSocket Client Due to Invalid server_max_window_bits Validation
743:  reason: not allowlisted
744:  advisory: https://github.com/advisories/GHSA-v9p9-hfj2-hcw8
745:  ##[error]Process completed with exit code 1.
746:  ##[group]Run failures=0
747:  �[36;1mfailures=0�[0m
748:  �[36;1m�[0m
749:  �[36;1mreport_blocking() {�[0m
750:  �[36;1m  name="$1"�[0m
751:  �[36;1m  outcome="$2"�[0m
752:  �[36;1m  if [ "$outcome" = "success" ]; then�[0m
753:  �[36;1m    echo "::notice::$name passed"�[0m
754:  �[36;1m  elif [ "$outcome" = "skipped" ]; then�[0m
755:  �[36;1m    echo "::warning::$name skipped"�[0m
756:  �[36;1m  else�[0m
757:  �[36;1m    echo "::error::$name failed"�[0m
758:  �[36;1m    failures=1�[0m
759:  �[36;1m  fi�[0m
760:  �[36;1m}�[0m
761:  �[36;1m�[0m
762:  �[36;1mreport_warning() {�[0m
763:  �[36;1m  name="$1"�[0m
764:  �[36;1m  outcome="$2"�[0m
765:  �[36;1m  if [ "$outcome" = "success" ]; then�[0m
766:  �[36;1m    echo "::notice::$name passed"�[0m
767:  �[36;1m  elif [ "$outcome" = "skipped" ]; then�[0m
768:  �[36;1m    echo "::warning::$name skipped"�[0m
769:  �[36;1m  else�[0m
770:  �[36;1m    echo "::warning::$name failed (warning only)"�[0m
771:  �[36;1m  fi�[0m
...

784:  �[36;1mif [ "$failures" -ne 0 ]; then�[0m
785:  �[36;1m  exit 1�[0m
786:  �[36;1mfi�[0m
787:  shell: /usr/bin/bash -e {0}
788:  env:
789:  PNPM_HOME: /home/runner/setup-pnpm/node_modules/.bin
790:  ##[endgroup]
791:  ##[notice]Lint passed
792:  ##[notice]Typecheck passed
793:  ##[notice]Convex raw auth guard passed
794:  ##[notice]Convex validator any guard passed
795:  ##[notice]Secret scan gate passed
796:  ##[notice]Security headers policy check passed
797:  ##[notice]Convex backend tests passed
798:  ##[notice]Web production build passed
799:  ##[error]Dependency audit gate failed
800:  ##[error]Process completed with exit code 1.
801:  Post job cleanup.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the inbox knowledge workflow to (1) use embedding-based vector search for more relevant results, and (2) insert knowledge-base article references as article:<id> links so the widget can open articles in-widget via data-article-id click handling. It also updates shared markdown sanitization to recognize article: links and includes related widget styling/spec documentation updates.

Changes:

  • Add knowledge:searchWithEmbeddings Convex action using vector search + embeddings and wire it into the inbox hook.
  • Standardize article link insertion to [title](article:<articleId>) and update shared markdown sanitization to emit data-article-id for widget navigation.
  • Add widget-side click handling for rendered message content and update widget styles/spec docs to support the new behavior.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/web-shared/src/markdown.ts Allow article: link protocol, emit data-article-id, and adjust link hardening/sanitization behavior.
packages/web-shared/src/markdown.test.ts Add tests validating article link detection and sanitization behavior.
packages/convex/convex/knowledge.ts Add searchWithEmbeddings action using embedding generation + vector search and enrich results.
apps/web/src/app/inbox/hooks/useInboxConvex.ts Replace reactive query-based knowledge search with action-based search managed via useEffect/useState.
apps/web/src/app/inbox/page.tsx Update knowledge insertion to use article:<id> link format (now also for internalArticle).
apps/web/src/app/inbox/InboxThreadPane.tsx Update knowledge picker UI actions to focus on inserting links (diff currently includes commented-out JSX).
apps/widget/src/components/conversationView/MessageList.tsx Add click handler to open widget article view when clicking an element with data-article-id.
apps/widget/src/styles.css Add .opencom-article-link styling and reformat transitions/gradients for readability.
openspec/specs/shared-markdown-rendering-sanitization/spec.md Document requirement for detecting article: links and emitting navigation metadata.
openspec/specs/inbox-knowledge-vector-search/spec.md New spec describing inbox vector-search requirements and workspace scoping.
openspec/specs/inbox-knowledge-insertion/spec.md Update spec to define article link insertion format and behavior.
openspec/specs/ai-help-center-linked-sources/spec.md Add requirement for consistent article link format with AI sources.
openspec/changes/archive/2026-03-13-article-link-widget-integration/tasks.md Archive task list for this change set.
openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/shared-markdown-rendering-sanitization/spec.md Archived spec delta for markdown article link detection.
openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/inbox-knowledge-vector-search/spec.md Archived spec delta for inbox vector search.
openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/inbox-knowledge-insertion/spec.md Archived spec delta for insertion behavior changes.
openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/ai-help-center-linked-sources/spec.md Archived spec delta for consistent article link formatting.
openspec/changes/archive/2026-03-13-article-link-widget-integration/proposal.md Archive proposal for the overall change.
openspec/changes/archive/2026-03-13-article-link-widget-integration/design.md Archive design notes including risks/trade-offs.
openspec/changes/archive/2026-03-13-article-link-widget-integration/.openspec.yaml Archive metadata for the change set.
Comments suppressed due to low confidence (1)

packages/web-shared/src/markdown.ts:146

  • Allowing the article protocol in hasDisallowedAbsoluteProtocol affects both links and images. Because this helper is also used for <img src>, markdown like ![](article:abc) will now keep an img with an article: src, which is likely unintended. Consider keeping article: allowed only for anchors (special-case in the <a> branch) while continuing to restrict <img> to http/https only.
function hasDisallowedAbsoluteProtocol(rawUrl: string): boolean {
  const match = rawUrl.trim().match(/^([a-z0-9+.-]+):/i);
  if (!match) {
    return false;
  }
  const protocol = match[1].toLowerCase();
  return protocol !== "http" && protocol !== "https" && protocol !== "article";
}

function isArticleLink(href: string): boolean {
  return href.trim().toLowerCase().startsWith("article:");
}

function extractArticleId(href: string): string | null {
  const match = href.trim().match(/^article:([a-zA-Z0-9]+)$/i);
  return match ? match[1] : null;
}

function enforceSafeLinksAndMedia(html: string, options: ResolvedParseMarkdownOptions): string {
  const container = document.createElement("div");
  container.innerHTML = html;

  container.querySelectorAll("a").forEach((anchor) => {
    const href = anchor.getAttribute("href");
    if (!href || hasBlockedProtocol(href)) {
      anchor.removeAttribute("href");
      anchor.removeAttribute("target");
      anchor.removeAttribute("rel");
      return;
    }

    if (isArticleLink(href)) {
      const articleId = extractArticleId(href);
      if (articleId) {
        anchor.setAttribute("data-article-id", articleId);
        anchor.setAttribute("class", "opencom-article-link");
        anchor.removeAttribute("href");
        anchor.removeAttribute("target");
        anchor.removeAttribute("rel");
      } else {
        anchor.removeAttribute("href");
        anchor.removeAttribute("target");
        anchor.removeAttribute("rel");
      }
      return;
    }

    if (hasDisallowedAbsoluteProtocol(href)) {
      anchor.removeAttribute("href");
      anchor.removeAttribute("target");
      anchor.removeAttribute("rel");
      return;
    }

    if (options.linkTarget === null) {
      anchor.removeAttribute("target");
    } else {
      anchor.setAttribute("target", options.linkTarget);
    }

    if (options.linkRel === null) {
      anchor.removeAttribute("rel");
    } else {
      anchor.setAttribute("rel", options.linkRel);
    }
  });

  container.querySelectorAll("img").forEach((image) => {
    const src = image.getAttribute("src");
    if (!src || hasBlockedProtocol(src) || hasDisallowedAbsoluteProtocol(src)) {
      image.removeAttribute("src");
    }
  });

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +105 to +118
if (isArticleLink(href)) {
const articleId = extractArticleId(href);
if (articleId) {
anchor.setAttribute("data-article-id", articleId);
anchor.setAttribute("class", "opencom-article-link");
anchor.removeAttribute("href");
anchor.removeAttribute("target");
anchor.removeAttribute("rel");
} else {
anchor.removeAttribute("href");
anchor.removeAttribute("target");
anchor.removeAttribute("rel");
}
return;
Comment on lines +271 to +276
function getShallowRunQuery(ctx: { runQuery: unknown }) {
return ctx.runQuery as unknown as <Args extends Record<string, unknown>, Return>(
queryRef: EmbeddingQueryRef<Args, Return>,
queryArgs: Args
) => Promise<Return>;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. getshallowrunquery chained cast 📘 Rule violation ✓ Correctness

The new helper getShallowRunQuery uses a chained escape cast (as unknown as ...) without any
justification comment. This expands unsafe type-escape usage in runtime code beyond the allowed
documented exceptions.
Agent Prompt
## Issue description
`getShallowRunQuery` introduces a chained type-escape cast (`as unknown as ...`) with no documentation. The rule requires these casts to be minimal, hotspot-local, and documented.

## Issue Context
This helper is used to call `ctx.runQuery(...)` inside `searchWithEmbeddings`. Ideally, type `ctx` using the correct Convex action context type (or a properly typed `runQuery` signature) to eliminate the cast; if a cast is unavoidable, add a `NOTE:` comment explaining why and how it can be removed.

## Fix Focus Areas
- packages/convex/convex/knowledge.ts[271-276]
- packages/convex/convex/knowledge.ts[282-299]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the inbox + widget knowledge-link workflow by switching inbox knowledge search to a vector/embeddings-based Convex action, standardizing inserted article links to an article:<id> format for widget navigation, and extending the shared markdown sanitizer to emit data-article-id metadata for those links.

Changes:

  • Add knowledge:searchWithEmbeddings (Convex action) to power inbox semantic knowledge search using vectorSearch + embeddings.
  • Change inbox “insert knowledge” behavior to insert article links as [title](article:<articleId>) and update shared markdown sanitization to preserve/annotate article: links.
  • Update widget message rendering to intercept clicks on [data-article-id] links and add styling for .opencom-article-link.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
security/dependency-audit-allowlist.json Adds allowlist entries for undici advisories (dev-only transitive deps).
packages/web-shared/src/markdown.ts Allows article: protocol through sanitization and annotates article links with data-article-id.
packages/web-shared/src/markdown.test.ts Adds tests for article: link handling + sanitization behavior.
packages/convex/tests/runtimeTypeHardeningGuard.test.ts Extends guard coverage to include the new knowledge vector-search implementation.
packages/convex/convex/knowledge.ts Introduces searchWithEmbeddings action using embeddings + vector search and typed refs.
openspec/specs/shared-markdown-rendering-sanitization/spec.md Documents new requirement for article-link metadata in shared markdown rendering.
openspec/specs/inbox-knowledge-vector-search/spec.md New spec for inbox vector search behavior + workspace scoping.
openspec/specs/inbox-knowledge-insertion/spec.md Updates insertion requirements to support [title](article:<articleId>) links.
openspec/specs/ai-help-center-linked-sources/spec.md Aligns agent-inserted article links with AI sources’ ID-based linking.
openspec/changes/archive/2026-03-13-article-link-widget-integration/tasks.md Archived task checklist for the change bundle.
openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/shared-markdown-rendering-sanitization/spec.md Archived spec delta for shared markdown updates.
openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/inbox-knowledge-vector-search/spec.md Archived spec delta for inbox vector search.
openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/inbox-knowledge-insertion/spec.md Archived spec delta for inbox insertion updates.
openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/ai-help-center-linked-sources/spec.md Archived spec delta for consistent link formatting.
openspec/changes/archive/2026-03-13-article-link-widget-integration/proposal.md Archived proposal describing rationale and scope.
openspec/changes/archive/2026-03-13-article-link-widget-integration/design.md Archived design notes and tradeoffs for article links + vector search.
openspec/changes/archive/2026-03-13-article-link-widget-integration/.openspec.yaml Adds archived change metadata.
apps/widget/src/styles.css Adds .opencom-article-link styling and reformats transitions/gradients.
apps/widget/src/components/conversationView/MessageList.tsx Adds click interception for data-article-id links to open articles in-widget.
apps/web/src/app/inbox/page.tsx Updates insertion format for article links to use article:<id>.
apps/web/src/app/inbox/hooks/useInboxConvex.ts Switches knowledge search from query to action and manages results via state/effect.
apps/web/src/app/inbox/InboxThreadPane.tsx Adjusts knowledge picker UI to focus on inserting links (button behavior changes).
AGENTS.md Adds guidance to run pnpm ci:check before opening a PR.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 214 to 222
return (
<Button size="sm" variant="outline" onClick={() => onInsertKnowledgeContent(item)}>
Insert Content
<Button
size="sm"
variant="outline"
onClick={() => onInsertKnowledgeContent(item, "link")}
>
<Link className="mr-1 h-3.5 w-3.5" />
Insert Link
</Button>
Comment on lines 384 to 391
if (item.type === "snippet") {
setInputValue((prev) => `${prev}${prev ? "\n\n" : ""}${item.content}`);
setLastInsertedSnippetId(item.id as Id<"snippets">);
} else if (action === "link" && item.type === "article" && item.slug) {
setInputValue((prev) => `${prev}${prev ? "\n\n" : ""}[${item.title}](/help/${item.slug})`);
} else if (action === "link" && item.type === "article") {
setInputValue((prev) => `${prev}${prev ? "\n\n" : ""}[${item.title}](article:${item.id})`);
} else {
setInputValue((prev) => `${prev}${prev ? "\n\n" : ""}${item.content}`);
}
Comment on lines +74 to +83
const handleMessageClick = (event: React.MouseEvent<HTMLDivElement>) => {
const target = event.target as HTMLElement;
const articleLink = target.closest("[data-article-id]");
if (articleLink) {
event.preventDefault();
event.stopPropagation();
const articleId = articleLink.getAttribute("data-article-id");
if (articleId) {
onSelectArticle(articleId as Id<"articles">);
}
@djanogly djanogly merged commit f75edd3 into dev Mar 14, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants