feat: custom agents - backend, frontend, settings, webhooks, and versioning#422
Conversation
📝 WalkthroughWalkthroughThis pull request introduces a comprehensive custom agents feature spanning the entire application stack. It adds frontend routes and UI components for creating, managing, and testing custom agents with configurable tools, webhooks, knowledge bases, and system instructions. Backend support includes new Convex queries and mutations for agent lifecycle management with versioning and publishing workflows. The PR also implements team-based filtering across agents, documents, and chat using a new context-based TeamFilterProvider. Built-in agent definitions are formalized with a registry system. Integration support for custom agents is added via bound integration tools. HTTP webhook endpoints enable external agent interaction. Authorization rules and role-based permissions are extended to support custom agent management. Minor proxy health check configuration updates redirect from port 80 to port 2020. Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 44
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
services/platform/convex/lib/attachments/process_attachments.ts (1)
316-321: 🧹 Nitpick | 🔵 TrivialRemove unused
imageInfoListandtextFileInfoListfields fromProcessedAttachmentsinterface.The
ProcessedAttachmentsinterface definesimageInfoListandtextFileInfoList, but they're always returned as empty arrays. The sole caller ofprocessAttachments(internal_actions.ts:172) only destructurespromptContent, never accessing these fields. Since images and text files are handled via their respective tools (as noted in the code comment), these interface fields serve no purpose and should be removed.services/platform/app/features/documents/components/onedrive-import-dialog.tsx (1)
28-473:⚠️ Potential issue | 🟡 MinorSync default team selection on dialog open to avoid stale preselect.
selectedTeamsis initialized once fromselectedTeamId. If the dialog remains mounted and the team filter changes before the next open, the preselect can drift. Consider resetting the default when the dialog opens.🔧 Suggested sync on open
-import { useState, useMemo, useCallback } from 'react'; +import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; @@ const { t } = useT('documents'); const { t: tCommon } = useT('common'); const { selectedTeamId } = useTeamFilter(); + const wasOpenRef = useRef(false); @@ const [selectedTeams, setSelectedTeams] = useState<Set<string>>( () => selectedTeamId ? new Set([selectedTeamId]) : new Set(), ); + + useEffect(() => { + const isOpen = props.open ?? false; + if (isOpen && !wasOpenRef.current) { + setSelectedTeams(selectedTeamId ? new Set([selectedTeamId]) : new Set()); + } + wasOpenRef.current = isOpen; + }, [props.open, selectedTeamId]);services/platform/convex/documents/queries.ts (1)
67-94:⚠️ Potential issue | 🔴 CriticalValidate
filterTeamIdagainstuserTeamIdsbefore querying.The
getDocumentsCursorhelper applies thefilterTeamIdas a document filter but does not validate that the user has access to that team. An org member could passfilterTeamIdfor a team they don't belong to and retrieve those documents.Add a guard in the
listDocumentshandler:Guard in listDocuments
const userTeamIds = await getUserTeamIds(ctx, authUser.userId); + + if (args.filterTeamId && !userTeamIds.includes(args.filterTeamId)) { + return { page: [], isDone: true, continueCursor: '' }; + } return await DocumentsHelpers.getDocumentsCursor(ctx, { ...args, userTeamIds, });
🤖 Fix all issues with AI agents
In `@services/platform/app/features/chat/components/agent-selector.tsx`:
- Around line 155-162: Replace the non-semantic separator div used when
hasCustomAgents and filteredBuiltin.length > 0 with a semantic <hr> element:
locate the conditional rendering around hasCustomAgents / filteredBuiltin in the
AgentSelector component (references: hasCustomAgents, filteredBuiltin) and
change the <div className="mx-2 my-1 border-t border-border" role="separator" />
to an <hr className="mx-2 my-1 border-t border-border" /> to preserve styling
and remove the redundant role attribute.
In
`@services/platform/app/features/custom-agents/components/custom-agent-create-dialog.tsx`:
- Around line 61-68: The form created with useForm<FormData> (resolver:
zodResolver(formSchema), mode: 'onChange') lacks explicit defaultValues, causing
fields to initialize as undefined; update the useForm call to provide a
defaultValues object matching FormData (e.g., empty strings/booleans/arrays as
appropriate) so controlled inputs registered via register and validation using
formSchema behave predictably and avoid undefined initial state for
isValid/isSubmitting/errors handling.
In
`@services/platform/app/features/custom-agents/components/custom-agent-delete-dialog.tsx`:
- Around line 9-16: The agent._id field in CustomAgentDeleteDialogProps is typed
as string causing an unsafe cast when calling the Convex mutation; change the
agent._id type to Id<'customAgents'> in the CustomAgentDeleteDialogProps
interface so it matches the mutation signature (remove the string type), then
remove the now-unnecessary `as any` cast where the mutation is invoked (the call
that uses agent._id in the delete mutation inside the CustomAgentDeleteDialog
component).
In
`@services/platform/app/features/custom-agents/components/custom-agent-knowledge.tsx`:
- Around line 103-115: The component is setting state during render (using
knowledgeEnabled, includeOrgKnowledge and initialized based on agent), causing
React warnings and stale UI; move this initialization into a useEffect that
depends on agentId (and/or agent) so state is set after render and is
reinitialized whenever agent/agentId changes, and remove the render-time set*
calls and instead setKnowledgeEnabled(agent?.knowledgeEnabled ?? false),
setIncludeOrgKnowledge(agent?.includeOrgKnowledge ?? false) and
setInitialized(true) inside that effect (also ensure to reset initialized to
false or set state appropriately when agentId changes).
In
`@services/platform/app/features/custom-agents/components/custom-agent-navigation.tsx`:
- Around line 102-164: The three handlers handleRollback, handlePublish, and
handleUnpublish duplicate the same async-pattern (set loading state, await
mutation, success toast, error toast, finally clear loading) — extract a
reusable helper (e.g., runAsyncAction or createAsyncHandler) that accepts the
action function (rollback/publishAgent/unpublishAgent), a loading-state setter
(setRollingBackVersion or setIsPublishing/setIsUnpublishing — for rollback
accept a setter that takes the targetVersion or null), success and error i18n
keys (t('customAgents.xxx')), and any args needed; replace the three handlers
with thin wrappers that call this helper to set state, call the mutation, and
show toast via toast and t to eliminate duplication and keep existing behavior.
- Around line 41-46: The CustomAgentNavigation component destructures
organizationId from its props but never uses it; remove organizationId from both
the CustomAgentNavigationProps type/interface and from the parameter
destructuring in the CustomAgentNavigation function signature so the prop is no
longer accepted or referenced, or alternatively use organizationId where
required—update the CustomAgentNavigationProps definition and the function
parameter list (CustomAgentNavigation) to stay consistent.
In
`@services/platform/app/features/custom-agents/components/custom-agent-row-actions.tsx`:
- Around line 15-34: The CustomAgentRowActionsProps interface declares many
unused fields; update it to depend on the shared CustomAgent type (or use Pick)
so only required properties are included: replace the inline agent shape in
CustomAgentRowActionsProps with either agent: Pick<CustomAgent, '_id' | 'name' |
'displayName' | 'rootVersionId'> or include any additional fields needed by the
delete dialog, ensuring references in CustomAgentRowActionsProps and components
using CustomAgentRowActionsProps still compile (check usages of rootVersionId,
_id, name, displayName and the delete dialog helper that may need extra fields).
In
`@services/platform/app/features/custom-agents/components/custom-agent-version-history-dialog.tsx`:
- Around line 86-100: Replace the three conditional Badge renderings for
version.status with a single mapping-driven render: add a statusBadgeConfig
Record (keyed by version.status values 'draft'|'active'|'archived') that maps to
variant and i18n labelKey, then lookup statusBadgeConfig[version.status] and, if
present, render one Badge with variant from the config and label via
t(config.labelKey); update usages of Badge and the JSX where version.status is
checked to use this mapping to simplify and centralize badge logic.
In
`@services/platform/app/features/custom-agents/components/custom-agent-webhook-section.tsx`:
- Line 68: The isPublished boolean is using the wrong property (versionNumber),
causing drafts with versionNumber > 0 to be treated as published; update the
logic that sets isPublished (in custom-agent-webhook-section.tsx — the
isPublished variable) to check the agent.status field instead (e.g., treat as
published only when agent.status === 'active'), ensuring you handle a missing
agent (agent null/undefined) safely.
In `@services/platform/app/features/custom-agents/components/test-chat-panel.tsx`:
- Around line 32-48: The duplicate-suppression key in canSendMessage currently
uses only threadId and trimmed content, causing attachments-only messages and
different agent/org panels to collide; update canSendMessage (and the same logic
at the other occurrence) to include attachment signatures/hash (e.g., compute a
deterministic signature from attachments) and agent/org identifiers in the key
alongside threadId and content, so recentSends and DUPLICATE_WINDOW_MS operate
per agent/org and per-attachment combination; keep the same sliding-cleanup of
recentSends entries and use the new composite key when reading/writing
recentSends.
In `@services/platform/app/features/custom-agents/components/tool-selector.tsx`:
- Line 54: The forEach callback currently implicitly returns the result of
Set.add (matched.forEach((t) => assigned.add(t))), which triggers a lint
warning; update the matched.forEach callback to use a block body that calls
assigned.add(t) without returning (or replace the forEach with an explicit
for..of loop that calls assigned.add for each item) so that assigned.add is
invoked but no value is implicitly returned.
- Around line 22-44: Replace the hardcoded TOOL_CATEGORIES with a derived
structure that imports TOOL_NAMES from "@/convex/agent_tools/tool_names" and
builds each category array from those constants (referencing TOOL_NAMES.* names)
so frontend stays in sync; update the top of tool-selector.tsx to import
TOOL_NAMES and construct TOOL_CATEGORIES programmatically (use the existing
category keys: CRM, Web, Documents, Knowledge, Workflows, Integrations, Data,
Assistants, Other) by mapping to the appropriate TOOL_NAMES entries (e.g.,
TOOL_NAMES.customer_read, TOOL_NAMES.web, etc.) instead of string literals,
ensuring all code paths that reference TOOL_CATEGORIES continue to work.
In
`@services/platform/app/features/documents/components/document-upload-dialog.tsx`:
- Around line 42-44: The dialog's close/open logic resets selectedTeams to an
empty Set which discards the pre-selected team from useTeamFilter; update the
reset in handleOpenChange (and any other reset sites like the onCancel/onClose
block around selectedTeams) to setSelectedTeams(() => selectedTeamId ? new
Set([selectedTeamId]) : new Set()) so the current selectedTeamId is re-applied
when the dialog re-opens (reference selectedTeams, setSelectedTeams,
handleOpenChange and selectedTeamId).
In `@services/platform/app/hooks/use-team-filter.tsx`:
- Around line 54-56: The hook currently reads localStorage inside the useState
initializer (selectedTeamId / setSelectedTeamIdRaw), which runs during SSR and
crashes; change to initialize state without accessing localStorage (e.g., null)
and move the localStorage read into a client-only effect (useEffect) that checks
for window or typeof localStorage !== 'undefined' and calls
setSelectedTeamIdRaw(storageValue) if present; also ensure any writes (where
storageKey is used) guard window and stay synchronous with state updates so
server-rendering no longer touches localStorage.
- Around line 65-69: When validatedTeamId is computed as null (i.e., the stored
selectedTeamId no longer exists in teams) you must synchronize that change back
to state and persistent storage: add a useEffect that watches validatedTeamId
and, when it === null, calls the state setter to set selectedTeamId to null and
also remove/update the corresponding localStorage entry so the stale id isn't
re-read on subsequent renders; reference the variables validatedTeamId,
selectedTeamId and teams when locating where to add this effect.
In `@services/platform/app/routes/dashboard/`$id/custom-agents.tsx:
- Around line 32-42: The current check treats memberContext === undefined and
memberContext === null the same and always renders a loading skeleton; change
the logic to handle the two states separately: when memberContext === undefined
(still loading) continue to render the Skeleton inside
StickyHeader/AdaptiveHeaderRoot, and when memberContext === null (explicitly no
membership) render an access-denied UI (a message or redirect) instead of the
skeleton. Locate the conditional using memberContext in this component and
replace the single null/undefined branch with two branches—one for undefined
(loading) and one for null (no member/access denied) while keeping the existing
StickyHeader/AdaptiveHeaderRoot skeleton for the loading branch.
In `@services/platform/app/routes/dashboard/`$id/custom-agents/$agentId.tsx:
- Around line 95-97: The Badge currently hardcodes tCommon('status.draft') and
therefore always shows "draft"; update it to render the agent's actual status by
using the agent's status field (e.g., agent.status or the prop/variable that
holds the fetched agent) and mapping that value to the proper translation via
tCommon (for example tCommon(`status.${agent.status}`)); ensure you handle
missing/undefined status with a sensible default and preserve the existing Badge
props (Badge component, className, variant).
In `@services/platform/app/routes/dashboard/`$id/custom-agents/$agentId/index.tsx:
- Around line 60-68: The code sets state during render when `agent` is
available; move this one-time initialization into a useEffect: create a
useEffect that depends on `agent` (and optionally `accessInitialized`) and
inside it call `setTeamId(agent.teamId || undefined)`,
`setSharedWithTeamIds(agent.sharedWithTeamIds ?? [])`, and
`setAccessInitialized(true)` only when not already initialized; remove the
direct `setTeamId`/`setSharedWithTeamIds`/`setAccessInitialized` calls from the
component body so state updates happen after render and avoid extra render
cycles or loops.
In `@services/platform/app/routes/dashboard/`$id/custom-agents/$agentId/tools.tsx:
- Around line 42-86: Extract the shared save pattern used in handleToolChange
and handleIntegrationBindingsChange into a reusable helper (e.g.,
saveWithStatus) that wraps setSaveStatus('saving'), awaits a provided update
function (calling updateAgent with the appropriate payload), and on success sets
'saved' or on error logs the error, sets 'error', and shows the toast with
t('customAgents.agentUpdateFailed'); then refactor handleToolChange to compute
finalTools (respecting agent?.knowledgeEnabled and 'rag_search') and call
saveWithStatus(() => updateAgent({ customAgentId: agentId as Id<'customAgents'>,
toolNames: finalTools })), and refactor handleIntegrationBindingsChange to call
saveWithStatus(() => updateAgent({ customAgentId: agentId as Id<'customAgents'>,
integrationBindings: bindings })); ensure saveWithStatus is memoized with
useCallback and depends on t so references to setSaveStatus, toast, and
updateAgent remain correct.
In
`@services/platform/convex/agent_tools/integrations/create_bound_integration_tool.ts`:
- Around line 83-93: The ApprovalResult interface is declared inside the
handler, causing it to be recreated each call—move the ApprovalResult
declaration to module scope (top of the file) so it’s defined once, and update
any references to it (including the isApprovalResult type guard) to use the
module-scoped ApprovalResult; ensure the isApprovalResult function remains at
module scope or can still reference the moved interface and that any
exports/imports are adjusted if other modules need this type.
- Around line 56-64: Move the organizationId validation before the async call to
getApprovalThreadId: check that ctx.organizationId exists and throw the existing
Error if missing prior to calling getApprovalThreadId(ctx, currentThreadId) so
you avoid invoking the async function unnecessarily; adjust the code around the
variables organizationId, currentThreadId, threadId and the call to
getApprovalThreadId accordingly.
In
`@services/platform/convex/agent_tools/integrations/fetch_operations_summary.ts`:
- Around line 42-75: The loop assumes integration.sqlOperations and
connectorConfig.operations always exist; update isSqlIntegration handling to
guard for a missing or undefined sqlOperations (e.g., check
Array.isArray(integration.sqlOperations) before iterating) and, when falling
back to predefined via getPredefinedIntegration, explicitly cast connector to
the expected dynamic type and guard connectorConfig?.operations with an
Array.isArray check before the for..of; ensure operations.push only runs when
the guarded arrays exist and keep the existing fields (name, title,
operationType, requiresApproval) intact.
In `@services/platform/convex/agents/builtin_agents.ts`:
- Around line 118-131: The toSerializableConfig function currently reads
process.env.OPENAI_CODING_MODEL directly when def.type === 'workflow'; change it
to obtain the workflow model from the shared runtime helper instead (e.g., call
getDefaultAgentRuntimeConfig() or a dedicated helper) and set config.model from
that returned config rather than process.env; update imports to include
getDefaultAgentRuntimeConfig (or the appropriate helper) and ensure
SerializableAgentConfig.model is populated only when the runtime helper provides
a value.
- Around line 24-25: BuiltinAgentType is a manually maintained union that can
drift from the canonical const array of builtin agent strings; change the type
to be derived from that const (e.g., use typeof <CONST_NAME>[number]) so the
union always reflects the source-of-truth list—locate the const array that lists
the builtin agents (the constant used around line ~133) and replace the explicit
union declaration of BuiltinAgentType with a type derived from that const
(referencing BuiltinAgentType and the const name in your change).
In `@services/platform/convex/custom_agents/chat.ts`:
- Around line 54-58: The loop variable `v` in the async iterator over
`activeVersionQuery` shadows the imported `v` validator from `convex/values`;
rename the loop variable to a non-conflicting identifier (e.g., `version` or
`activeV`) in the for-await-of loop that assigns `activeVersion` (references:
`activeVersionQuery`, `activeVersion`), and update any uses inside that loop
accordingly so the imported `v` remains available.
In `@services/platform/convex/custom_agents/mutations.ts`:
- Around line 164-212: The parameter named customAgentId on updateCustomAgent is
misleading because it is actually used as a rootVersionId (via getDraftByRoot);
update the API to avoid confusion by either renaming the arg to rootVersionId
throughout this mutation (change the args entry from customAgentId to
rootVersionId, update the v.id validator usage, update destructuring const {
rootVersionId: _, teamId, ...otherFields } and all references passed to
getDraftByRoot and ctx/db calls) or, if you prefer not to change the external
API, add a clear JSDoc on the updateCustomAgent handler and the customAgentId
arg stating that this value is the draft rootVersionId and will be passed to
getDraftByRoot; apply the same rename-or-JSDoc change consistently for all other
mutations in this file that currently accept customAgentId but call
getDraftByRoot.
- Around line 384-447: The rollbackCustomAgentVersion mutation currently deletes
the existing draft (ctx.db.delete(draft._id)) when creating a new draft from the
target version, which can discard unsaved UI changes; add a clear JSDoc comment
above the rollbackCustomAgentVersion function explaining that it removes the
current draft and that unsaved edits will be lost, reference the behavior that
ctx.db.delete(draft._id) is invoked and that a new draft is created from
copyVersionFields(targetVersion), and optionally mention adding a future
flag/parameter (e.g., preserveDraft) or requiring a UI confirmation to prevent
accidental loss.
In `@services/platform/convex/custom_agents/queries.ts`:
- Around line 125-133: The getAvailableTools query currently returns TOOL_NAMES
without any auth; update its handler to require an authenticated context like
other queries by accepting the query context (e.g., args, ctx or session) and
checking for a logged-in user or session (throwing an authorization error if
absent). Keep the return shape the same (mapping TOOL_NAMES to {name, available:
true}) but gate the handler with the same auth pattern used elsewhere in this
file (match the auth helper or session property used by other queries so
getAvailableTools enforces authentication consistently).
- Around line 70-74: The for-await loop uses a loop variable named `v` which
shadows the imported `v` from `convex/values`; change the loop variable in the
iteration over `draftQuery` (the `for await (const v of draftQuery)` line) to a
non-conflicting name such as `row`, `item`, or `draftRow`, and update its use
inside the loop (assign to `draft`) so the top-level import `v` remains
unshadowed and available for later validation.
- Around line 23-33: The current ternary sets status = args.filterPublished ?
'active' : 'draft' which makes undefined behave like false; update the logic
around args.filterPublished in queries.ts (the status variable and the
ctx.db.query('customAgents').withIndex('by_org_active_status' block) to make the
default explicit: handle three cases (true -> 'active', false -> 'draft',
undefined -> no status filter) by computing status only when
args.filterPublished is strictly true/false and only calling .eq('status',
status) on the query when status is defined; reference the status variable,
args.filterPublished, getUserTeamIds, and withIndex('by_org_active_status') to
locate the change.
In `@services/platform/convex/custom_agents/schema.ts`:
- Around line 17-56: The schema currently defines several compound indexes on
customAgentsTable (by_organization_active, by_org_active_status,
by_root_status); remove or comment out these compound .index(...) calls and keep
only single-field indexes (by_organization, by_root, by_team, by_creator) for
now, and document that compound indexes can be re-added later if profiling or
known UI query patterns (e.g., listing active agents per org) prove they are
needed; reference the .index('by_organization_active', ['organizationId',
'isActive']), .index('by_org_active_status', ['organizationId', 'isActive',
'status']), and .index('by_root_status', ['rootVersionId', 'status']) calls when
locating the changes.
In `@services/platform/convex/custom_agents/test_chat.test.ts`:
- Around line 73-131: The tests duplicate the RAG team ID construction logic in
multiple it blocks; move the existing buildRagTeamIds helper (currently defined
later) up into the describe('RAG team ID construction') scope and replace the
inline construction in each test with calls to buildRagTeamIds(draft) (where
drafts are created with createMockDraftAgent). Ensure you remove the duplicated
ragTeamIds arrays and conditional pushes in each test and use buildRagTeamIds to
produce the array, then keep the same expect assertions.
In `@services/platform/convex/custom_agents/test_chat.ts`:
- Around line 54-58: The for-await loop uses the loop variable named "v" which
shadows the imported "v" validator from convex/values; rename the loop variable
(e.g., to "row", "item", or "draftItem") in the iteration over "draftQuery" so
the imported "v" remains available and unshadowed — update any references inside
the loop (and the assignment to "draft") to use the new variable name.
In `@services/platform/convex/custom_agents/webhooks/http_actions.ts`:
- Around line 27-33: The code currently derives token from URL pathname segments
(using url.pathname.split('/') into pathParts and taking the last element) which
fails when pathname ends with a trailing slash; update the extraction to
normalize segments first by filtering out empty strings (e.g., pathParts =
url.pathname.split('/').filter(Boolean)) and then set token =
pathParts[pathParts.length - 1]; keep the same error branch using jsonResponse({
error: 'Missing webhook token' }, 400) if token is still falsy.
In `@services/platform/convex/custom_agents/webhooks/mutations.ts`:
- Around line 48-70: After verifying membership with getOrganizationMember in
the toggleWebhook handler, call authorizeRls(membership?.role, 'customAgents',
'write') (using the membership returned from getOrganizationMember) before
performing ctx.db.patch to ensure role-based write permission; make the same
change in deleteWebhook (call authorizeRls with membership?.role,
'customAgents', 'write' after getOrganizationMember and before ctx.db.delete) so
both mutations enforce the RLS-style authorization.
In `@services/platform/convex/custom_agents/webhooks/queries.ts`:
- Around line 8-11: The code is using .collect() on the Convex query result (the
webhooks variable from ctx.db.query('customAgentWebhooks').withIndex('by_agent',
(q) => q.eq('customAgentId', args.customAgentId))), which violates the
repository guideline; replace the .collect() call by iterating the query with a
for await loop over ctx.db.query('customAgentWebhooks').withIndex('by_agent',
...) and either push each item into your webhooks array or handle items inline;
ensure the new loop yields the same array or behavior previously produced by
webhooks so downstream code that references webhooks continues to work.
- Line 17: The getWebhooks query is returning full webhook tokens without
authorization; update the getWebhooks resolver to call
authComponent.getAuthUser() to ensure the request is authenticated, retrieve the
caller's team IDs via getUserTeamIds(authUser.id), and enforce access by using
hasTeamAccess(teamIds, wh.teamId) (or equivalent) before including wh.token in
the response; only return tokens for webhooks the caller is authorized to access
and otherwise omit or filter out unauthorized webhooks, mirroring the pattern
used in getCustomAgent and getDocumentById.
In `@services/platform/convex/documents/migrate_team_fields.ts`:
- Around line 57-61: The migrated counter is incremented unconditionally even
when no DB patch occurs; change the logic so that migrated is incremented only
when a patch is actually applied: check Object.keys(patch).length > 0 (the same
condition used for ctx.db.patch) and move or perform migrated++ inside that
block after awaiting ctx.db.patch(doc._id, patch) so only documents where a
mutation occurred are counted; reference the variables patch, migrated,
ctx.db.patch and doc._id to locate the correct spot.
- Around line 35-39: Replace the unsafe cast (args.cursor as any) used in the
loop comparison with an explicit string conversion to preserve type-safety:
convert args.cursor to a string (e.g., via String(args.cursor)) when comparing
to doc._id inside the for-await loop in migrate_team_fields.ts, and remove the
as any cast; if possible, also narrow args.cursor's declared type where it’s
defined so the comparison no longer requires conversion at the use site.
In `@services/platform/convex/documents/team_fields.ts`:
- Around line 8-11: The interface UnifiedTeamFields is declared but not
exported, which prevents callers from reusing its type; export the interface by
adding an export before the declaration (export interface UnifiedTeamFields) so
consumers of teamTagsToUnifiedFields can import and use the type in their
variables or function signatures.
In `@services/platform/convex/lib/agent_response/generate_response.ts`:
- Around line 356-358: Extract the repeated system-prompt concatenation into a
small helper function named buildSystemPrompt(instructions: string | undefined,
threadContext: string): string that returns instructions ?
`${instructions}\n\n${threadContext}` : threadContext; then replace the three
duplicated expressions that construct a system prompt (the occurrences that
assign to variables like systemPrompt, retrySystemPrompt, and finalSystemPrompt
or where you concatenate instructions and threadContext using
`${instructions}\n\n${threadContext}`) with calls to
buildSystemPrompt(instructions, threadContext) to centralize the logic and avoid
duplication.
In `@services/platform/convex/lib/attachments/process_attachments.ts`:
- Around line 286-294: The failed-* lists currently match attachments by
fileName which breaks when names duplicate; change the matching to use fileId
instead by ensuring ParsedDocument, analyzedImages, and analyzedTextFiles
include fileId and then compare documentAttachments/fileAttachments by fileId
(e.g., update the logic that builds failedDocuments, failedImages,
failedTextFiles to check a.fileId against parsedDocuments.some(d => d.fileId) /
analyzedImages.some(d => d.fileId) / analyzedTextFiles.some(d => d.fileId));
this also lets you replace O(n²) name-based scans with constant-time lookups
(build a Set of processed fileIds before filtering).
In `@services/platform/lib/shared/schemas/custom_agents.ts`:
- Around line 16-58: Add a new boolean field knowledgeEnabled to both
customAgentSchema and createCustomAgentSchema so the validators accept the UI
payload; update customAgentSchema (z.object) to include knowledgeEnabled:
z.boolean().optional() (or required if expected) alongside
includeOrgKnowledge/knowledgeTopK, and add the same knowledgeEnabled declaration
to createCustomAgentSchema to allow create/update requests to pass validation;
modify the TypeScript type inference (CustomAgent) will automatically reflect
the change via z.infer<typeof customAgentSchema>.
In `@services/platform/messages/en.json`:
- Around line 681-684: The help text "Unique identifier (lowercase, hyphens
allowed)" is inaccurate because the backend schema uses name: v.string() with no
character restrictions; either update the UI copy or add server-side validation
to match it: (A) Quick fix — change the locales entry nameHelp to a truthful
message like "Unique identifier (any characters allowed)" referencing the
"nameHelp" key; (B) Preferred fix — enforce the documented constraint by
updating the validation schema (the place where name: v.string() is defined and
the related mutation handler) to a regex (e.g., /^[a-z0-9-]+$/) and return a
clear validation error message sent to the client so the UI displays a matching
error for the name field. Ensure the localized help text and server validation
stay consistent.
…ate in webhook section
…ge suppression key
…d dialog re-opens
…I_CODING_MODEL access
…om customAgents schema
Id<T> requires T extends TableNames | SystemTableNames, but the generic was constrained to string, causing TS2344 in CI.
Fix useEffect exhaustive-deps warning by adding missing `agent` dependency, run formatter on 19 unformatted files, remove unused exports/dependencies (fuse.js, oxlint-tsgolint, dead code in service-worker, convex-server, pagination types, tone-of-voice types, and custom-agents schemas).
…stom agents Introduce URL-based version navigation (?v=N), a shared version context provider, and read-only mode for non-draft versions. Replace rollback with explicit activate and create-draft-from-version mutations. Publishing no longer auto-creates a new draft.
…plication for custom agents Add configurable file preprocessing that pre-analyzes attachments via a beforeGenerate hook so the AI can respond directly from extracted content. Introduce publish/deactivate/activate row actions in the agent table with a confirmation dialog for deactivation. Replace the version column with a status badge and deduplicate the agent list by root ID (showing the best status per agent). Remove the unused 'vision' model preset. Fix the test chat loading state to bridge the gap between mutation return and agent streaming start.
…rsion Rename testDraftCustomAgent to testCustomAgent so users can test any version (draft, active, or archived) instead of only draft agents.
Summary
Test plan
custom_agents/test_chat.test.ts)🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Improvements
Translations