Skip to content

feat(platform): openai-compat agent webhook for meetily integration#1587

Merged
larryro merged 3 commits into
mainfrom
feat/openai-compat-webhook-meetily
Apr 20, 2026
Merged

feat(platform): openai-compat agent webhook for meetily integration#1587
larryro merged 3 commits into
mainfrom
feat/openai-compat-webhook-meetily

Conversation

@larryro
Copy link
Copy Markdown
Collaborator

@larryro larryro commented Apr 20, 2026

Summary

Adds a per-agent OpenAI ChatCompletion endpoint at POST /api/agents/wh/<TOKEN>/chat/completions that reuses the existing agent webhook path. Meetily (and any other OpenAI-compatible client) can now configure the webhook URL as its Base URL and get a real agent conversation back — system prompt, tools, conversation thread, governance, audit log, and usage ledger all applied.

We explicitly chose NOT to extend /api/v1/chat/completions: that endpoint stays a strict provider pass-through so its 100% OpenAI compatibility is preserved. Agent identity travels in the URL token, never in the model field — no split-brain semantics between the two endpoints.

Highlights

  • Branch in agentWebhookHandler on path suffix; token parsed at fixed position and validated against ^[0-9a-f]{64}$.
  • Client system message(s) concatenated after the agent's own systemInstructions with \n\n---\n\n; client text capped at 50k chars (413 on overflow).
  • Errors use the OpenAI {error:{message,type,code,param}} envelope via the shared openAIErrorResponse helper.
  • user (legacy) and safety_identifier (current OpenAI spec) both honored as stable caller identifier; sha256-hashed into a new agentWebhookUserThreads(webhookId, userHash) -> threadId table so repeated POSTs with the same identifier land in the same thread.
  • Lookup-or-create runs inside a single internal mutation (getOrCreateUserThread) — atomic under Convex OCC.
  • Cascade-deletes mapping rows on webhook delete and thread delete.
  • Webhook records a createdByUserId at creation so webhook-generated threads surface in the creator's chat history.
  • New agentType: 'openai_webhook' (added to AGENT_CONTEXT_CONFIGS) distinguishes the wire format in audit logs without changing existing per-agent analytics.
  • Usage dialog in the agent Webhook tab now shows OpenAI-compat curl and Meetily config snippets; i18n strings for en/de/fr added.
  • Meeting-transcription tutorial (en/de/fr) rewritten to point at the per-agent webhook URL, add a webhook-creation step, drop stale "Advanced preset" language, and correct client-timeout / conv-thread wording.

Security fix (applies to both legacy and new paths)

startWebhookChat now validates that a client-supplied threadId belongs to the webhook's organization before using it. Closes a pre-existing cross-org bug where a token holder in org A could post into a known threadId from org B.

Test plan

  • Unit tests: path parsing, token regex, content extraction (string / array / null), system-prompt merge, thread-deletion cascade — 32 new tests.
  • Typecheck (npx tsc --noEmit) clean.
  • Lint (npm run lint --workspace=@tale/platform) clean.
  • Mintlify broken-link check on rewritten docs — clean.
  • Manual curl against local dev: 200 + valid ChatCompletion envelope with real usage from messageMetadata.
  • Manual error-path tests: 400 no-user-message, 400 malformed JSON, 401 bad token, 404 unknown sub-path, 400 empty messages.
  • Regression: legacy {message} body on base URL unchanged; /api/v1/chat/completions passthrough unchanged.
  • Thread grouping verified end-to-end: same user → same thread with history; different user → distinct threads.
  • Meetily end-to-end (record a meeting → generate summary) — to be run by the reviewer.

Summary by CodeRabbit

Release Notes

  • New Features

    • Agent webhooks now support OpenAI-compatible ChatCompletion API requests
    • Meetily meeting transcription integration now uses webhook URLs instead of API keys for enhanced security
  • Documentation

    • Updated meeting transcription tutorials with webhook-based configuration steps
    • Added OpenAI-compatible endpoint usage examples

larryro added 2 commits April 20, 2026 21:04
Adds a per-agent OpenAI ChatCompletion endpoint at
`POST /api/agents/wh/<TOKEN>/chat/completions` that reuses the existing
agent webhook path. Meetily (or any OpenAI-compat client) configures the
webhook URL as its Base URL and gets a real agent conversation back —
with the agent's system prompt, tools, conversation thread, governance,
audit log, and usage ledger all applied.

Why a new sub-path rather than extending `/api/v1/chat/completions`:
that endpoint is a strict provider pass-through and must stay 100%
OpenAI-compatible. Agent identity lives in the URL token, never in the
`model` field, so there is no split-brain semantics between the two
endpoints.

Highlights:
- `agentWebhookHandler` branches on path suffix; token parsed at fixed
  position and validated against `^[0-9a-f]{64}$`.
- Client `system` message(s) concatenated after the agent's own
  `systemInstructions` with `\n\n---\n\n`; client text capped at 50k
  chars (413 on overflow).
- Errors use the OpenAI `{error:{message,type,code,param}}` envelope
  via the shared `openAIErrorResponse` helper.
- `user` (legacy) and `safety_identifier` (current OpenAI spec) are
  both honored as a stable caller identifier; sha256-hashed into a new
  `agentWebhookUserThreads(webhookId, userHash) -> threadId` table so
  repeated POSTs with the same identifier land in the same thread.
- Lookup-or-create runs inside a single internal mutation
  (`getOrCreateUserThread`) — atomic under Convex OCC.
- Cascade-deletes mapping rows on webhook delete and thread delete.
- Webhook records a `createdByUserId` at creation so that
  webhook-generated threads surface in the creator's chat history.
- New `agentType: 'openai_webhook'` (added to the AGENT_CONTEXT_CONFIGS
  map) distinguishes the wire format in audit logs without changing
  existing per-agent analytics.
- Usage dialog in the agent Webhook tab now shows OpenAI-compat curl
  and Meetily config snippets; i18n strings added for en/de/fr.
- Meeting-transcription tutorial (en/de/fr) rewritten to point at the
  per-agent webhook URL, add a webhook-creation step, drop the stale
  "Advanced preset" language, and correct the client-timeout / conv-
  thread wording.

Security fix (also affects the legacy `{message, threadId?}` path):
`startWebhookChat` now validates that a client-supplied `threadId`
belongs to the webhook's organization before using it. Closes a
pre-existing cross-org bug where a token holder in org A could post
into a known threadId from org B.

Tests: 32 new unit tests cover path parsing, token regex, content
extraction (string/array/null), system-prompt merge, thread-deletion
cascade. Typecheck, lint, and Mintlify broken-links all clean.
Terminology linter flags unlocalized "Settings" in DE and FR docs.
These references point at Meetily's (external) UI label, not Tale's,
so they should be treated as literal strings rather than translated.
Wrap in backticks (the existing exemption style for external UI refs)
instead of bold. Also refactor one compound word in DE to use the
localised term ("Summary-Einstellungen").
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

📝 Walkthrough

Walkthrough

This PR restructures agent webhook integration from a Tale API key-based model to a webhook URL-based architecture supporting OpenAI-compatible endpoints. Changes include: updating documentation to reflect Meetily configuration via webhook URLs instead of API keys; expanding webhook routing to handle both legacy (POST /api/agents/wh/<TOKEN>) and OpenAI-compatible (POST /api/agents/wh/<TOKEN>/chat/completions) request formats; introducing a persistent agentWebhookUserThreads table to map repeated client requests to stable conversation threads; implementing system prompt merging that concatenates client and agent prompts; extending the webhook schema with createdByUserId tracking; adding cascade deletion for webhook user threads; and registering an openai_webhook agent context configuration.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: implementing OpenAI-compatible webhook support for Meetily integration in the agent platform.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/openai-compat-webhook-meetily

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/de/tutorials/admin/meeting-transcription.md`:
- Line 83: The docs currently state a disabled or invalid webhook returns 401;
update the sentence in meeting-transcription.md to distinguish cases: say a
disabled webhook returns 403 (Forbidden) while an invalid token returns 401
(Unauthorized), matching the behavior implemented in http_actions.ts (the OpenAI
webhook handling path). Adjust the troubleshooting advice to check for a 403
when the webhook is disabled and for 401 when the token is invalid.

In `@docs/fr/tutorials/admin/meeting-transcription.md`:
- Line 83: Update the documentation to reflect the correct HTTP status for a
disabled webhook: change the troubleshooting entry that currently lists **401
Unauthorized** for a disabled webhook to **403 Forbidden**; reference the
behavior implemented in services/platform/convex/agents/webhooks/http_actions.ts
(the OpenAI webhook handling path) which returns 401 only for invalid tokens and
returns 403 when a webhook is disabled so readers are guided to check webhook
activation rather than token validity.

In `@docs/tutorials/admin/meeting-transcription.md`:
- Line 83: The docs entry incorrectly groups disabled webhooks under "401
Unauthorized"; update the sentence in
docs/tutorials/admin/meeting-transcription.md to document disabled webhooks as
"403 Forbidden" instead of 401 and adjust the troubleshooting hint accordingly
(mention Settings > agent > Webhook tab: toggle active, or delete and
regenerate) so it matches the actual mapping implemented in
services/platform/convex/agents/webhooks/http_actions.ts (where 401 is only
returned for invalid tokens on the OpenAI path and disabled webhooks are
returned as 403). Ensure the wording clearly differentiates invalid token (401)
vs. disabled webhook (403).

In `@services/platform/convex/agents/webhooks/http_actions.ts`:
- Around line 461-486: fetchUsageForThread currently queries getMessageMetadata
with messageId: '' which returns the latest metadata for a thread and can return
the wrong turn when threads are reused; change the flow so chatViaWebhook
returns the generated assistant messageId (or metadata id) and update
fetchUsageForThread to accept that messageId and call
api.message_metadata.queries.getMessageMetadata with the concrete messageId
(instead of '') to fetch prompt/output/total and model for that specific turn;
update callers of fetchUsageForThread/chatViaWebhook to pass and propagate the
returned messageId accordingly (references: fetchUsageForThread, chatViaWebhook,
api.message_metadata.queries.getMessageMetadata, messageId).

In `@services/platform/convex/agents/webhooks/internal_mutations.ts`:
- Around line 211-219: The current helper collects all matching
agentWebhookUserThreads into memory via .collect(), which can OOM on large
datasets; replace the collect() + for loop with an async stream and delete rows
one-by-one. Specifically, remove the .collect() call and iterate using for await
over ctx.db.query('agentWebhookUserThreads').withIndex('by_webhookId_userHash',
q => q.eq('webhookId', args.webhookId')).stream(), calling await
ctx.db.delete(row._id) for each streamed row so deletion is done incrementally.

In `@services/platform/convex/agents/webhooks/mutations.ts`:
- Around line 88-98: The code currently collects the entire
agentWebhookUserThreads query result with .collect(), which can OOM for large
tables; change it to stream results using an async iterator (for await) over
ctx.db.query('agentWebhookUserThreads').withIndex('by_webhookId_userHash', q =>
q.eq('webhookId', args.webhookId)) and delete each row as it is yielded via
ctx.db.delete(row._id) instead of materializing into mappings; ensure you remove
the .collect() call and use a for await (const row of ...) loop around
ctx.db.delete(row._id).

In `@services/platform/convex/threads/delete_chat_thread.ts`:
- Around line 36-42: The code currently uses .collect() on the Convex query
result (webhookMappings = await
ctx.db.query('agentWebhookUserThreads').withIndex('by_threadId', (q) =>
q.eq('threadId', threadId)).collect()), which pulls the entire set into memory;
change this to iterate the query result with a for await loop over
ctx.db.query('agentWebhookUserThreads').withIndex('by_threadId', (q) =>
q.eq('threadId', threadId)) and inside the loop call ctx.db.delete(row._id) for
each row (keep the same delete call and variable name row._id) so rows are
processed lazily and memory usage stays bounded.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c5e05937-a628-4b4e-9473-512c77638c07

📥 Commits

Reviewing files that changed from the base of the PR and between cb02129 and fedc61c.

📒 Files selected for processing (19)
  • docs/de/tutorials/admin/meeting-transcription.md
  • docs/fr/tutorials/admin/meeting-transcription.md
  • docs/tutorials/admin/meeting-transcription.md
  • services/platform/app/features/agents/components/agent-webhook-section.tsx
  • services/platform/convex/agents/webhooks/http_actions.test.ts
  • services/platform/convex/agents/webhooks/http_actions.ts
  • services/platform/convex/agents/webhooks/internal_actions.test.ts
  • services/platform/convex/agents/webhooks/internal_actions.ts
  • services/platform/convex/agents/webhooks/internal_mutations.ts
  • services/platform/convex/agents/webhooks/mutations.ts
  • services/platform/convex/agents/webhooks/schema.ts
  • services/platform/convex/lib/context_management/constants.ts
  • services/platform/convex/openai_compat/response_format.ts
  • services/platform/convex/schema.ts
  • services/platform/convex/threads/delete_chat_thread.test.ts
  • services/platform/convex/threads/delete_chat_thread.ts
  • services/platform/messages/de.json
  • services/platform/messages/en.json
  • services/platform/messages/fr.json

- **Leere Zusammenfassung** — das Transkript ist in Tale angekommen, aber der Agent hat abgelehnt. Den Konversations-Thread in der Tale-UI auf die tatsächliche Modell-Antwort prüfen; wurde der Agent durch [Richtlinien](/de/platform/admin/governance) gesperrt, steht der Grund dort.
- **401 Unauthorized** — API-Schlüssel widerrufen oder vertippt; in **Einstellungen > API-Schlüssel** neu erzeugen.
- **Leere oder abgelehnte Zusammenfassung** — den Konversations-Thread des Agents in Tale prüfen; die vollständige Modell-Antwort (inklusive Ablehnungs- oder Governance-Meldungen) steht dort.
- **401 Unauthorized** — die Webhook-URL ist ungültig oder der Webhook ist deaktiviert. Prüfe Einstellungen > Agent > **Webhook**-Tab; aktiv schalten oder löschen und neu erstellen.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Dokumentiere deaktivierte Webhooks als 403 statt 401.

services/platform/convex/agents/webhooks/http_actions.ts gibt im OpenAI-Pfad nur für ungültige Tokens 401 zurück; ein deaktivierter Webhook wird mit 403 beantwortet. So wie es hier steht, landet das Troubleshooting beim falschen Fehlerbild.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/de/tutorials/admin/meeting-transcription.md` at line 83, The docs
currently state a disabled or invalid webhook returns 401; update the sentence
in meeting-transcription.md to distinguish cases: say a disabled webhook returns
403 (Forbidden) while an invalid token returns 401 (Unauthorized), matching the
behavior implemented in http_actions.ts (the OpenAI webhook handling path).
Adjust the troubleshooting advice to check for a 403 when the webhook is
disabled and for 401 when the token is invalid.

- **Résumé vide** — la transcription est arrivée à Tale mais l'agent a refusé. Regarde le thread de conversation dans l'UI Tale pour voir la vraie réponse du modèle ; si l'agent a été bloqué par les [règles de gouvernance](/fr/platform/admin/governance), la raison y apparaît.
- **401 Unauthorized** — clé API révoquée ou mal saisie ; régénérer dans **Paramètres > Clés API**.
- **Résumé vide ou refusé** — inspecte le thread de conversation de l'agent dans Tale ; la réponse complète du modèle (y compris les refus ou messages de gouvernance) y apparaît.
- **401 Unauthorized** — l'URL du webhook est invalide ou le webhook est désactivé. Vérifie Paramètres > agent > onglet **Webhook** ; bascule en actif, ou supprime et régénère.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Documente le webhook désactivé en 403, pas en 401.

Dans services/platform/convex/agents/webhooks/http_actions.ts, le chemin OpenAI renvoie 401 uniquement pour un token invalide ; un webhook désactivé renvoie 403. Tel quel, ce dépannage oriente vers le mauvais statut HTTP.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/fr/tutorials/admin/meeting-transcription.md` at line 83, Update the
documentation to reflect the correct HTTP status for a disabled webhook: change
the troubleshooting entry that currently lists **401 Unauthorized** for a
disabled webhook to **403 Forbidden**; reference the behavior implemented in
services/platform/convex/agents/webhooks/http_actions.ts (the OpenAI webhook
handling path) which returns 401 only for invalid tokens and returns 403 when a
webhook is disabled so readers are guided to check webhook activation rather
than token validity.

- **Empty summary** — the transcript reached Tale but the agent refused. Check the conversation thread in the Tale UI for the actual model response; if the agent was gated by [governance rules](/platform/admin/governance), the reason appears there.
- **401 Unauthorized** — API key was revoked or mistyped; regenerate in **Settings > API Keys**.
- **Empty or refused summary** — inspect the agent's conversation thread in Tale; the full model response (including any refusal or governance message) appears there.
- **401 Unauthorized** — the webhook URL is invalid or the webhook is disabled. Check Settings > agent > **Webhook** tab; toggle active, or delete and regenerate.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Document disabled webhooks as 403, not 401.

services/platform/convex/agents/webhooks/http_actions.ts returns 401 only for invalid tokens on the OpenAI path; a disabled webhook is mapped to 403. Keeping both cases under “401 Unauthorized” will send users down the wrong troubleshooting path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/tutorials/admin/meeting-transcription.md` at line 83, The docs entry
incorrectly groups disabled webhooks under "401 Unauthorized"; update the
sentence in docs/tutorials/admin/meeting-transcription.md to document disabled
webhooks as "403 Forbidden" instead of 401 and adjust the troubleshooting hint
accordingly (mention Settings > agent > Webhook tab: toggle active, or delete
and regenerate) so it matches the actual mapping implemented in
services/platform/convex/agents/webhooks/http_actions.ts (where 401 is only
returned for invalid tokens on the OpenAI path and disabled webhooks are
returned as 403). Ensure the wording clearly differentiates invalid token (401)
vs. disabled webhook (403).

Comment on lines +461 to +486
async function fetchUsageForThread(
ctx: ActionCtx,
threadId: string,
): Promise<{ usage: OpenAIUsage; model: string | null }> {
try {
const meta = await ctx.runQuery(
api.message_metadata.queries.getMessageMetadata,
{
messageId: '',
threadId,
},
);
if (!meta) {
return { usage: ZERO_USAGE, model: null };
}
const prompt = meta.inputTokens ?? 0;
const completion = meta.outputTokens ?? 0;
const total = meta.totalTokens ?? prompt + completion;
return {
usage: {
prompt_tokens: prompt,
completion_tokens: completion,
total_tokens: total,
},
model: meta.model ?? null,
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Lookup usage by turn, not by thread.

The OpenAI path now intentionally reuses a thread per userHash, but fetchUsageForThread() calls getMessageMetadata with messageId: '', which forces the query to fall back to “latest metadata in this thread”. On a reused or concurrent thread, that can return the previous or next turn’s inputTokens, outputTokens, and model, so the ChatCompletion response can report usage/model for the wrong completion. Return the generated assistant messageId (or metadata id) from chatViaWebhook and query metadata for that specific turn instead.

Possible fix direction
- let chatResult: { threadId: string; streamId: string };
+ let chatResult: { threadId: string; streamId: string; messageId: string };

- async function fetchUsageForThread(
-   ctx: ActionCtx,
-   threadId: string,
- ): Promise<{ usage: OpenAIUsage; model: string | null }> {
+ async function fetchUsageForMessage(
+   ctx: ActionCtx,
+   messageId: string,
+   threadId: string,
+ ): Promise<{ usage: OpenAIUsage; model: string | null }> {
    const meta = await ctx.runQuery(
      api.message_metadata.queries.getMessageMetadata,
      {
-       messageId: '',
+       messageId,
        threadId,
      },
    );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/agents/webhooks/http_actions.ts` around lines 461 -
486, fetchUsageForThread currently queries getMessageMetadata with messageId: ''
which returns the latest metadata for a thread and can return the wrong turn
when threads are reused; change the flow so chatViaWebhook returns the generated
assistant messageId (or metadata id) and update fetchUsageForThread to accept
that messageId and call api.message_metadata.queries.getMessageMetadata with the
concrete messageId (instead of '') to fetch prompt/output/total and model for
that specific turn; update callers of fetchUsageForThread/chatViaWebhook to pass
and propagate the returned messageId accordingly (references:
fetchUsageForThread, chatViaWebhook,
api.message_metadata.queries.getMessageMetadata, messageId).

Comment on lines +211 to +219
const rows = await ctx.db
.query('agentWebhookUserThreads')
.withIndex('by_webhookId_userHash', (q) =>
q.eq('webhookId', args.webhookId),
)
.collect();

for (const row of rows) {
await ctx.db.delete(row._id);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and read the relevant section
fd -t f "internal_mutations.ts" services/platform/convex/

Repository: tale-project/tale

Length of output: 2269


🏁 Script executed:

# Get file size first
wc -l services/platform/convex/agents/webhooks/internal_mutations.ts

Repository: tale-project/tale

Length of output: 127


🏁 Script executed:

# Read lines around 211-219 to verify the issue
sed -n '205,225p' services/platform/convex/agents/webhooks/internal_mutations.ts

Repository: tale-project/tale

Length of output: 567


🏁 Script executed:

# Check for all .collect() usages in the file
rg "\.collect\(\)" services/platform/convex/agents/webhooks/internal_mutations.ts -n

Repository: tale-project/tale

Length of output: 82


Avoid .collect() in the webhook-thread cascade helper.

This cleanup path is unbounded by webhook usage, so collecting all rows into memory first can cause deletion to fail on large datasets. Stream the index and delete row-by-row with for await.

♻️ Proposed fix
-    const rows = await ctx.db
-      .query('agentWebhookUserThreads')
-      .withIndex('by_webhookId_userHash', (q) =>
-        q.eq('webhookId', args.webhookId),
-      )
-      .collect();
-
-    for (const row of rows) {
+    for await (const row of ctx.db
+      .query('agentWebhookUserThreads')
+      .withIndex('by_webhookId_userHash', (q) =>
+        q.eq('webhookId', args.webhookId),
+      )) {
       await ctx.db.delete(row._id);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const rows = await ctx.db
.query('agentWebhookUserThreads')
.withIndex('by_webhookId_userHash', (q) =>
q.eq('webhookId', args.webhookId),
)
.collect();
for (const row of rows) {
await ctx.db.delete(row._id);
for await (const row of ctx.db
.query('agentWebhookUserThreads')
.withIndex('by_webhookId_userHash', (q) =>
q.eq('webhookId', args.webhookId),
)) {
await ctx.db.delete(row._id);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/agents/webhooks/internal_mutations.ts` around lines
211 - 219, The current helper collects all matching agentWebhookUserThreads into
memory via .collect(), which can OOM on large datasets; replace the collect() +
for loop with an async stream and delete rows one-by-one. Specifically, remove
the .collect() call and iterate using for await over
ctx.db.query('agentWebhookUserThreads').withIndex('by_webhookId_userHash', q =>
q.eq('webhookId', args.webhookId')).stream(), calling await
ctx.db.delete(row._id) for each streamed row so deletion is done incrementally.

Comment on lines +88 to +98
// Cascade: remove any user→thread mapping rows for this webhook so the
// table doesn't accumulate orphans referencing a deleted webhookId.
const mappings = await ctx.db
.query('agentWebhookUserThreads')
.withIndex('by_webhookId_userHash', (q) =>
q.eq('webhookId', args.webhookId),
)
.collect();
for (const row of mappings) {
await ctx.db.delete(row._id);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the file
fd -t f "mutations.ts" services/platform/convex/agents/webhooks/

Repository: tale-project/tale

Length of output: 177


🏁 Script executed:

# Check file size and read the relevant section
wc -l services/platform/convex/agents/webhooks/mutations.ts

Repository: tale-project/tale

Length of output: 118


🏁 Script executed:

# Read lines 80-105 to see the context
sed -n '80,105p' services/platform/convex/agents/webhooks/mutations.ts

Repository: tale-project/tale

Length of output: 766


🏁 Script executed:

# Search for all .collect() usages in this file
rg "\.collect\(\)" services/platform/convex/agents/webhooks/mutations.ts -n

Repository: tale-project/tale

Length of output: 81


Stream mapping cleanup instead of collecting it.

The code materializes the entire agentWebhookUserThreads result set with .collect() before deletion. Since this table grows with every distinct caller, webhook deletion becomes an unbounded in-memory load. Use for await to stream and delete rows as they arrive.

♻️ Proposed fix
-    const mappings = await ctx.db
-      .query('agentWebhookUserThreads')
-      .withIndex('by_webhookId_userHash', (q) =>
-        q.eq('webhookId', args.webhookId),
-      )
-      .collect();
-    for (const row of mappings) {
+    for await (const row of ctx.db
+      .query('agentWebhookUserThreads')
+      .withIndex('by_webhookId_userHash', (q) =>
+        q.eq('webhookId', args.webhookId),
+      )) {
       await ctx.db.delete(row._id);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Cascade: remove any user→thread mapping rows for this webhook so the
// table doesn't accumulate orphans referencing a deleted webhookId.
const mappings = await ctx.db
.query('agentWebhookUserThreads')
.withIndex('by_webhookId_userHash', (q) =>
q.eq('webhookId', args.webhookId),
)
.collect();
for (const row of mappings) {
await ctx.db.delete(row._id);
}
// Cascade: remove any user→thread mapping rows for this webhook so the
// table doesn't accumulate orphans referencing a deleted webhookId.
for await (const row of ctx.db
.query('agentWebhookUserThreads')
.withIndex('by_webhookId_userHash', (q) =>
q.eq('webhookId', args.webhookId),
)) {
await ctx.db.delete(row._id);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/agents/webhooks/mutations.ts` around lines 88 - 98,
The code currently collects the entire agentWebhookUserThreads query result with
.collect(), which can OOM for large tables; change it to stream results using an
async iterator (for await) over
ctx.db.query('agentWebhookUserThreads').withIndex('by_webhookId_userHash', q =>
q.eq('webhookId', args.webhookId)) and delete each row as it is yielded via
ctx.db.delete(row._id) instead of materializing into mappings; ensure you remove
the .collect() call and use a for await (const row of ...) loop around
ctx.db.delete(row._id).

Comment on lines +36 to +42
const webhookMappings = await ctx.db
.query('agentWebhookUserThreads')
.withIndex('by_threadId', (q) => q.eq('threadId', threadId))
.collect();
for (const row of webhookMappings) {
await ctx.db.delete(row._id);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Replace .collect() with for await iteration.

The coding guidelines explicitly prohibit .collect() in Convex files because it pulls the entire result set into memory. Use for await to iterate over the query results instead.

♻️ Proposed fix
-  const webhookMappings = await ctx.db
+  const webhookMappings = ctx.db
     .query('agentWebhookUserThreads')
-    .withIndex('by_threadId', (q) => q.eq('threadId', threadId))
-    .collect();
-  for (const row of webhookMappings) {
+    .withIndex('by_threadId', (q) => q.eq('threadId', threadId));
+  for await (const row of webhookMappings) {
     await ctx.db.delete(row._id);
   }

As per coding guidelines: "No .collect() in Convex. Iterate with for await. collect pulls the whole result set into memory and scales badly."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const webhookMappings = await ctx.db
.query('agentWebhookUserThreads')
.withIndex('by_threadId', (q) => q.eq('threadId', threadId))
.collect();
for (const row of webhookMappings) {
await ctx.db.delete(row._id);
}
const webhookMappings = ctx.db
.query('agentWebhookUserThreads')
.withIndex('by_threadId', (q) => q.eq('threadId', threadId));
for await (const row of webhookMappings) {
await ctx.db.delete(row._id);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/threads/delete_chat_thread.ts` around lines 36 - 42,
The code currently uses .collect() on the Convex query result (webhookMappings =
await ctx.db.query('agentWebhookUserThreads').withIndex('by_threadId', (q) =>
q.eq('threadId', threadId)).collect()), which pulls the entire set into memory;
change this to iterate the query result with a for await loop over
ctx.db.query('agentWebhookUserThreads').withIndex('by_threadId', (q) =>
q.eq('threadId', threadId)) and inside the loop call ctx.db.delete(row._id) for
each row (keep the same delete call and variable name row._id) so rows are
processed lazily and memory usage stays bounded.

When the OpenAI-compat webhook request body sets `model`, we now forward
it as `resolveAgentConfig`'s `modelId` arg instead of silently ignoring
it. The existing `applyModelOverride` helper validates the value against
the agent's `supportedModels` allowlist (stripping the provider
qualifier, so `openrouter:openai/gpt-4o` and `openai/gpt-4o` both
match). Unrecognised values silently fall back to the agent's default
(`supportedModels[0]`), preserving the safe behavior we had before.

This makes the endpoint behave like a well-mannered OpenAI-compatible
server: clients that care can pick a specific model from a known
allowlist; clients that don't (Meetily, which sends arbitrary
placeholders) still get something sensible.

Docs and Usage dialog copy updated for EN/DE/FR so Meetily users know
to enter a real model ID if they want a specific one.
@larryro larryro merged commit 92f4aa4 into main Apr 20, 2026
15 checks passed
@larryro larryro deleted the feat/openai-compat-webhook-meetily branch April 20, 2026 13:21
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.

1 participant