feat(platform): workflow human input approvals#821
Conversation
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
📝 WalkthroughWalkthroughThis PR refactors the approval system's status model from Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 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 docstrings
🧪 Generate unit tests (beta)
📝 Coding Plan
Comment Tip You can customize the tone of the review comments and chat replies.Configure the |
There was a problem hiding this comment.
Actionable comments posted: 17
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (7)
services/platform/convex/workflow_engine/action_defs/workflow_processing_records/workflow_processing_records_action.ts (1)
79-83:⚠️ Potential issue | 🟡 MinorUpdate the example label to match the new status value.
The sample now filters
status == "completed", but the bullet still says “Find approved product recommendations”. That keeps the old enum name in the docs and makes the example harder to copy correctly.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@services/platform/convex/workflow_engine/action_defs/workflow_processing_records/workflow_processing_records_action.ts` around lines 79 - 83, Update the example label that precedes the filter for product approvals in the Examples block of workflow_processing_records_action.ts: change the bullet text "Find approved product recommendations" to match the new enum value (e.g., "Find completed product recommendations") so it aligns with the filterExpression 'status == "completed" && resourceType == "product_recommendation" used in the example; ensure the label and the example remain consistent in the Examples section near the top of the file.services/platform/app/features/custom-agents/components/test-chat-panel.tsx (1)
33-60:⚠️ Potential issue | 🟠 MajorPass the active approval state into the test composer too.
activeApprovalis only wired intoTestMessageList.TestChatInputstill receives no state that would disable the composer or swap its copy while a human-input/approval card is pending, so the test panel can keep sending normal chat messages during a paused workflow. That makes this surface behave differently from the main chat flow introduced in this PR.Also applies to: 92-119
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@services/platform/app/features/custom-agents/components/test-chat-panel.tsx` around lines 33 - 60, The TestChatInput composer in TestChatPanel isn't receiving the activeApproval state so it can be disabled or show alternate copy during pending approvals; update the TestChatPanel JSX where TestChatInput is rendered to pass the activeApproval prop (from useTestChat) into TestChatInput (and any related props like isDisabled or approvalState) so the input mirrors the paused workflow behavior used by TestMessageList; ensure all instances/usages of TestChatInput in this file (lines near where displayItems... and the TestMessageList/TestChatInput components are used) receive the activeApproval prop and the composer uses it to disable input or change copy.services/platform/convex/agent_tools/integrations/internal_actions.ts (1)
136-169:⚠️ Potential issue | 🔴 CriticalAdd an atomic execution claim before calling the integration.
status === "executing"is only a shared state check. If this action is invoked twice while the approval is stillexecuting, both callers will pass the guard and runexecuteIntegration(...), which can duplicate non-idempotent writes in the downstream system. Mirror the document-write path and atomically claim/setexecutedAtbefore Line 160 so only one executor can cross the side-effect boundary.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@services/platform/convex/agent_tools/integrations/internal_actions.ts` around lines 136 - 169, The current guard on approval.status === "executing" is a race; before calling internal.agent_tools.integrations.internal_actions.executeIntegration you must perform an atomic claim/update on the approval document (e.g., in the same Convex transaction API used elsewhere for document writes) to set an executedAt timestamp (or flip a claimed flag) only if executedAt is not already set, and throw/abort if the conditional update fails; update the approval record (using the same approval id from approval.id / approval._id) and then call ctx.runAction only after the transaction confirms you successfully claimed/updated executedAt so only one caller can proceed to executeIntegration.services/platform/convex/agent_tools/workflows/__tests__/execute_approved_workflow_update.test.ts (1)
173-181: 🧹 Nitpick | 🔵 TrivialTest name slightly misleading after status model change.
The test is named "throws when approval status is not approved" but now tests rejection of
pendingstatus. Consider renaming to "throws when approval status is not executing" to align with the new lifecycle model.📝 Suggested rename
- it('throws when approval status is not approved', async () => { + it('throws when approval status is not executing', async () => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@services/platform/convex/agent_tools/workflows/__tests__/execute_approved_workflow_update.test.ts` around lines 173 - 181, Rename the test description so it reflects the new lifecycle model: change the it(...) string from "throws when approval status is not approved" to "throws when approval status is not executing" in the test that calls getHandler(), createMockApproval({ status: 'pending' }), and asserts handler(ctx, { approvalId: 'approval-1', approvedBy: 'user-1' }) rejects; no code logic changes required—only update the test name to reference "executing".services/platform/app/features/chat/components/integration-approval-card.tsx (1)
269-292:⚠️ Potential issue | 🟡 MinorTranslate the badge label too.
Line 291 still renders the raw status code (
executing,completed, etc.), so the approval state is only partially localized even though the surrounding copy now uses translation keys.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@services/platform/app/features/chat/components/integration-approval-card.tsx` around lines 269 - 292, Badge currently shows the raw status string (status) which bypasses translations; change the rendering to use the same translated label used in the Text block by computing a translated status label (e.g., statusLabel via t('statusExecuting') / t('statusCompletedFailed') / t('statusCompletedSuccess') / t('statusRejected') based on the status and executionError) and render that variable inside the <Badge> instead of {status}; update the logic near the HStack/Badge in integration-approval-card.tsx (reference variables: status, executionError, t) so both the caption Text and the Badge show the localized status.services/platform/app/features/chat/components/human-input-request-card.tsx (1)
142-163:⚠️ Potential issue | 🟡 MinorDisable the card-level option wrappers while submission is in flight.
The inner
RadioGroupItem/Checkboxis disabled, but the surrounding button still updatesselectedValue/selectedValues. During a pending submit the card can visually switch to a different answer than the one already sent. MirrorisSubmittingon the wrapper click path as well.Also applies to: 198-209
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@services/platform/app/features/chat/components/human-input-request-card.tsx` around lines 142 - 163, The card-level wrapper buttons still call setSelectedValue / setSelectedValues even when isSubmitting, so the UI can change while a submit is pending; update the onClick handlers for the option wrappers (the button elements inside the RadioGroup and the corresponding checkbox wrapper used around metadata.options and the block around lines matching the second occurrence) to no-op when isSubmitting is true (i.e., guard the onClick with if (isSubmitting) return or early exit) so selection only changes when not submitting, mirroring the disabled state applied to the inner RadioGroupItem/Checkbox.services/platform/app/features/chat/components/workflow-creation-approval-card.tsx (1)
552-574:⚠️ Potential issue | 🟡 MinorTranslate the badge label instead of showing raw status codes.
The caption is localized, but the badge still renders the raw enum (
executing,completed,rejected). That leaks backend status codes into non-English UIs and leaves this card partially untranslated.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@services/platform/app/features/chat/components/workflow-creation-approval-card.tsx` around lines 552 - 574, The badge currently renders the raw status enum (status) and must show the localized label instead; update the Badge children to use the translation function (t) like the caption does (e.g., map 'executing' -> t('statusExecuting'), 'completed' -> t('statusCompletedSuccess' or 'statusCompletedFailed' depending on executionError, and 'rejected' -> t('statusRejected')) or extract that mapping into a helper (e.g., getStatusLabel(status, executionError)) and use it inside the Badge so the UI shows translated status strings instead of raw backend codes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/integrations/outlook/connector.ts`:
- Around line 386-402: Update the misleading comment to reflect that results are
fetched in descending order by receivedDateTime (orderby: receivedDateTime desc)
and ensure we always trim any over-fetched results down to requestedTop;
specifically, after the cursor handling in the block that references
cursor?.messageId, keep the existing findIndex/slice behavior when the cursor is
found, and add a fallback that sets rawValues = rawValues.slice(0, requestedTop)
when the cursor message is not found (so rawValues never contains more than
requestedTop); refer to the variables cursor, cursor.messageId, rawValues, and
requestedTop when making these changes.
In `@services/platform/app/features/chat/components/chat-messages.tsx`:
- Around line 172-187: The visibility predicate for messages (shouldShow)
currently only checks message.role, message.content, and message.isAborted,
which causes assistant messages that have empty content but non-empty file
payloads to be hidden; update the shouldShow logic used before rendering
MessageBubble to also treat messages with message.attachments.length > 0 or
message.fileParts.length > 0 as visible so assistant messages containing files
reach the MessageBubble component (preserving existing role normalization and
threadId injection).
In
`@services/platform/app/features/chat/components/document-write-approval-card.tsx`:
- Around line 78-83: The current flow calls updateApprovalStatus({ approvalId,
status: 'executing' }) and then separately calls executeDocumentWrite({
approvalId }), which can leave the approval stuck in 'executing' if
executeDocumentWrite fails or the client disconnects; make the transition atomic
by moving the status update into the executeDocumentWrite backend path or by
introducing a single server-side claim-and-execute endpoint (e.g., combine
updateApprovalStatus and execution inside executeDocumentWrite or create
executeAndClaimApproval(approvalId)). Update the client to call only the new
atomic endpoint (remove the separate updateApprovalStatus call) and ensure the
backend performs a transactional claim (set status to executing), runs the
write, and updates final status or reverts on failure so retries/rejects remain
possible.
In
`@services/platform/app/features/chat/components/workflow-run-approval-card.tsx`:
- Around line 287-295: The delayed clear in the onSuccess callback of
workflow-run-approval-card.tsx uses setTimeout without cancellation or
validation, so if a new approval appears within 500ms it will wipe the new
input; fix by capturing the timeout id and cancelling it when the active
approval changes or the component unmounts (use an effect cleanup), or instead
check the current approval identifier (e.g., approval.id / activeApprovalId)
inside the timeout and only call setHumanInputValue / setHumanInputSelected /
setHumanInputMulti if the approval id still matches the one that scheduled the
timeout.
In
`@services/platform/app/features/chat/components/workflow-update-approval-card.tsx`:
- Around line 232-236: The disclosure summary currently outputs hardcoded
English labels ("steps", "Step:", "unknown") when rendering based on
metadata.updateType; update the rendering in the component that uses
metadata.updateType / metadata.stepsConfig / metadata.steps / metadata.stepName
to instead use localized strings from the existing workflowUpdateApproval i18n
object (e.g., workflowUpdateApproval.stepsLabel,
workflowUpdateApproval.stepLabel, workflowUpdateApproval.unknownLabel),
replacing the raw literals so all branches use translated text while preserving
the numeric fallback logic.
In
`@services/platform/app/features/chat/hooks/__tests__/use-merged-chat-items.test.ts`:
- Around line 24-36: The test fixture makeApproval currently sets metadata to {}
as never which forces callers to cast arrays with `as never`; instead give
makeApproval a minimal explicit return type (e.g., an Approval or
Partial<Approval> shape matching what useMergedChatItems expects) and remove the
`as never` coercion, then at each call site use the TypeScript `satisfies`
operator to assert the fixture shape rather than using `as`; update
makeApproval's signature and returned object shape (including metadata and
_creationTime) and replace callers that use `as never` (also the fixtures around
lines 80-84) to use `satisfies` so test data stays type-safe and will break if
useMergedChatItems changes.
In `@services/platform/convex/agent_tools/workflows/internal_mutations.ts`:
- Around line 451-455: The mapping that builds steps is performing a redundant
shallow clone via Object.fromEntries(Object.entries(s.stepUpdates)); update the
steps mapping in the function that constructs the steps array (the lines
building steps: args.steps.map((s) => ({ ... }))) to use s.stepUpdates directly
(i.e., assign stepUpdates: s.stepUpdates) instead of the
Object.fromEntries(Object.entries(...)) no-op clone to simplify and avoid
unnecessary work.
- Around line 28-40: The claimWorkflowApprovalForExecution handler currently
only sets executedAt and misses transitioning status to 'executing'; update the
logic in claimWorkflowApprovalForExecution to atomically check approval.status
=== 'pending' (and that executedAt is unset), then patch the record setting both
executedAt: Date.now() and status: 'executing' in a single ctx.db.patch call (or
return false if status is not 'pending'), ensuring the claim moves the approval
from 'pending' → 'executing' and prevents races.
In `@services/platform/convex/agent_tools/workflows/update_workflow_step_tool.ts`:
- Around line 267-285: Add explicit validation to reject mixed single-step and
batch payloads: after computing isBatch (using Array.isArray(args.steps)), check
if isBatch is true but args.stepRecordId or args.updates is also provided (or
conversely if not isBatch but both steps and single-step fields are present) and
return a failure response with a clear message indicating that mixed payloads
are not allowed; update the validation logic surrounding isBatch, args.steps,
args.stepRecordId, and args.updates in update_workflow_step_tool.ts to fail-fast
when both batch and single-step fields are present.
- Around line 325-353: Move per-entry config validation to after fetching the
step info: for each sanitizedEntries entry, call
ctx.runQuery(internal.wf_step_defs.internal_queries.getStepWithWorkflowInfo, {
stepId: toId<'wfStepDefs'>(entry.stepRecordId) }) and if the lookup fails or
returns null, catch it and push a structured validation error for that entry
(don’t throw). Then compute the resolved step type as entry.updates.stepType ??
info.step.stepType and run validateStepConfig/validateStepUpdates against that
resolved type so config-only edits are validated correctly; keep collecting
warnings/errors into the existing validationErrors/validationWarnings arrays and
return them at the call boundary.
In `@services/platform/convex/approvals/helpers.ts`:
- Around line 231-252: The current listActiveApprovalsByOrganization visits
statuses sequentially and breaks when result reaches limit, causing newer
'executing' approvals to be skipped; change the logic in
listActiveApprovalsByOrganization to first collect approvals from both status
buckets without enforcing a per-status early break (remove the per-status
result.length >= limit breaks), then after the nested loops perform
result.sort((a,b)=>b._creationTime - a._creationTime) and finally slice to the
requested limit (e.g., result = result.slice(0, limit)); keep using the same
ctx.db query withIndex('by_org_status') and the status loop but ensure you only
apply the global limit after merging and sorting.
In `@services/platform/convex/lib/variables/replace_variables_in_string.ts`:
- Around line 37-40: The code preserves unresolved templates in
replaceVariablesInString but replaceVariables currently throws if any markers
remain; choose multi-pass templating: remove the fail-fast throw in
replaceVariables (or replace it with a non-fatal warning/return) so preserved
`{{...}}` tokens from replaceVariablesInString can be handled by downstream
passes; update the logic in the replaceVariables function to allow remaining
template markers instead of throwing, and add a comment explaining multi-pass
behavior referencing replaceVariablesInString and replaceVariables.
In `@services/platform/convex/wf_executions/mutations.ts`:
- Around line 97-119: The code trusts execution.triggerData.approvalId to pick
the thread to notify, which allows caller-supplied triggerData to route
cancellation messages; instead, load the approval via
ctx.db.get(toId<'approvals'>(approvalIdStr)) only as a candidate and verify it
is actually associated with this execution before calling saveMessage (e.g.,
compare approval.executionId or approval.workflowRunId/approval.workflowSlug
against this execution.id or execution.workflowSlug), and skip saveMessage if
the approval does not belong to the current execution; keep using
components.agent and the same message payload but only after the ownership check
to prevent spoofed/stale IDs from writing to unrelated threads.
In
`@services/platform/convex/workflow_engine/action_defs/conversation/conversation_action.ts`:
- Around line 19-26: The file places the const debugLog =
createDebugLog('DEBUG_WORKFLOW', '[Conversations]') between import statements;
move that declaration so all imports come first (i.e., after the import block
containing createConversation and the json validators). Specifically, remove the
inline call between imports and re-add const debugLog = createDebugLog(...)
immediately after the last import (references: createDebugLog, debugLog,
createConversation, jsonRecordValidator, jsonValueValidator) so the file has
only imports up top followed by the debugLog declaration.
In
`@services/platform/convex/workflow_engine/helpers/engine/execute_step_handler.ts`:
- Around line 212-223: The current logic uses pendingApprovals.find(...) which
can pick an older approval if duplicates exist; update the selection to
deterministically choose the newest matching human-input approval by filtering
pendingApprovals for resourceType === 'human_input_request' and stepSlug ===
args.stepSlug, then pick the one with the latest created timestamp (or largest
_id if _id encodes time) and assign its _id to approvalTaskId; alternatively
(preferred) change the tool path that issues request_human_input to return the
created approval ID directly and use that value instead of scanning
pendingApprovals.
In
`@services/platform/convex/workflow_engine/helpers/nodes/llm/utils/build_human_input_context.ts`:
- Around line 29-35: escapeForContext only converts < and >; extend it to also
replace newline characters with the literal "\n" (so the Q/A lines remain
single-line) and escape double quotes (e.g., replace " with " or \" ) so
quoted strings in the prompt cannot break formatting; update escapeForContext
and ensure formatResponse continues to call it (symbols: escapeForContext,
formatResponse, build_human_input_context) so all user questions/responses are
normalized before being inserted into the <human_input_context> block.
In `@services/platform/messages/en.json`:
- Around line 2155-2163: Update the wording under the workflowRunApproval block
so the rejection/cancellation terms match: change the status key value for
"statusRejected" to use the same "Cancelled" wording as "rejectTooltip" ("Cancel
workflow execution") — locate the workflowRunApproval JSON object and update the
"statusRejected" string to "Cancelled" (or the exact cancelled phrasing used by
creation/update approval cards) to keep tooltip and terminal status consistent.
---
Outside diff comments:
In `@services/platform/app/features/chat/components/human-input-request-card.tsx`:
- Around line 142-163: The card-level wrapper buttons still call
setSelectedValue / setSelectedValues even when isSubmitting, so the UI can
change while a submit is pending; update the onClick handlers for the option
wrappers (the button elements inside the RadioGroup and the corresponding
checkbox wrapper used around metadata.options and the block around lines
matching the second occurrence) to no-op when isSubmitting is true (i.e., guard
the onClick with if (isSubmitting) return or early exit) so selection only
changes when not submitting, mirroring the disabled state applied to the inner
RadioGroupItem/Checkbox.
In
`@services/platform/app/features/chat/components/integration-approval-card.tsx`:
- Around line 269-292: Badge currently shows the raw status string (status)
which bypasses translations; change the rendering to use the same translated
label used in the Text block by computing a translated status label (e.g.,
statusLabel via t('statusExecuting') / t('statusCompletedFailed') /
t('statusCompletedSuccess') / t('statusRejected') based on the status and
executionError) and render that variable inside the <Badge> instead of {status};
update the logic near the HStack/Badge in integration-approval-card.tsx
(reference variables: status, executionError, t) so both the caption Text and
the Badge show the localized status.
In
`@services/platform/app/features/chat/components/workflow-creation-approval-card.tsx`:
- Around line 552-574: The badge currently renders the raw status enum (status)
and must show the localized label instead; update the Badge children to use the
translation function (t) like the caption does (e.g., map 'executing' ->
t('statusExecuting'), 'completed' -> t('statusCompletedSuccess' or
'statusCompletedFailed' depending on executionError, and 'rejected' ->
t('statusRejected')) or extract that mapping into a helper (e.g.,
getStatusLabel(status, executionError)) and use it inside the Badge so the UI
shows translated status strings instead of raw backend codes.
In `@services/platform/app/features/custom-agents/components/test-chat-panel.tsx`:
- Around line 33-60: The TestChatInput composer in TestChatPanel isn't receiving
the activeApproval state so it can be disabled or show alternate copy during
pending approvals; update the TestChatPanel JSX where TestChatInput is rendered
to pass the activeApproval prop (from useTestChat) into TestChatInput (and any
related props like isDisabled or approvalState) so the input mirrors the paused
workflow behavior used by TestMessageList; ensure all instances/usages of
TestChatInput in this file (lines near where displayItems... and the
TestMessageList/TestChatInput components are used) receive the activeApproval
prop and the composer uses it to disable input or change copy.
In `@services/platform/convex/agent_tools/integrations/internal_actions.ts`:
- Around line 136-169: The current guard on approval.status === "executing" is a
race; before calling
internal.agent_tools.integrations.internal_actions.executeIntegration you must
perform an atomic claim/update on the approval document (e.g., in the same
Convex transaction API used elsewhere for document writes) to set an executedAt
timestamp (or flip a claimed flag) only if executedAt is not already set, and
throw/abort if the conditional update fails; update the approval record (using
the same approval id from approval.id / approval._id) and then call
ctx.runAction only after the transaction confirms you successfully
claimed/updated executedAt so only one caller can proceed to executeIntegration.
In
`@services/platform/convex/agent_tools/workflows/__tests__/execute_approved_workflow_update.test.ts`:
- Around line 173-181: Rename the test description so it reflects the new
lifecycle model: change the it(...) string from "throws when approval status is
not approved" to "throws when approval status is not executing" in the test that
calls getHandler(), createMockApproval({ status: 'pending' }), and asserts
handler(ctx, { approvalId: 'approval-1', approvedBy: 'user-1' }) rejects; no
code logic changes required—only update the test name to reference "executing".
In
`@services/platform/convex/workflow_engine/action_defs/workflow_processing_records/workflow_processing_records_action.ts`:
- Around line 79-83: Update the example label that precedes the filter for
product approvals in the Examples block of
workflow_processing_records_action.ts: change the bullet text "Find approved
product recommendations" to match the new enum value (e.g., "Find completed
product recommendations") so it aligns with the filterExpression 'status ==
"completed" && resourceType == "product_recommendation" used in the example;
ensure the label and the example remain consistent in the Examples section near
the top of the file.
🪄 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: 51f70001-5d7b-4928-a466-a5b9da4b7930
⛔ Files ignored due to path filters (1)
services/platform/convex/_generated/api.d.tsis excluded by!**/_generated/**
📒 Files selected for processing (69)
.claude/CLAUDE.mdexamples/integrations/outlook/connector.tsexamples/workflows/contract-generation/config.jsonservices/platform/app/features/automations/components/automation-assistant/message-list.test.tsxservices/platform/app/features/automations/components/automation-assistant/message-list.tsxservices/platform/app/features/automations/executions/executions-table.tsxservices/platform/app/features/chat/components/approval-card-renderer.tsxservices/platform/app/features/chat/components/chat-input.tsxservices/platform/app/features/chat/components/chat-interface.tsxservices/platform/app/features/chat/components/chat-messages.tsxservices/platform/app/features/chat/components/document-write-approval-card.tsxservices/platform/app/features/chat/components/human-input-request-card.tsxservices/platform/app/features/chat/components/integration-approval-card.tsxservices/platform/app/features/chat/components/workflow-creation-approval-card.tsxservices/platform/app/features/chat/components/workflow-run-approval-card.tsxservices/platform/app/features/chat/components/workflow-update-approval-card.tsxservices/platform/app/features/chat/hooks/__tests__/use-merged-chat-items.test.tsservices/platform/app/features/chat/hooks/__tests__/use-message-processing.test.tsservices/platform/app/features/chat/hooks/queries.tsservices/platform/app/features/chat/hooks/use-execution-status.tsservices/platform/app/features/chat/hooks/use-merged-chat-items.tsservices/platform/app/features/chat/hooks/use-message-processing.tsservices/platform/app/features/custom-agents/components/test-chat-panel.tsxservices/platform/app/features/custom-agents/components/test-chat-panel/test-message-list.tsxservices/platform/app/features/custom-agents/hooks/use-test-chat.tsservices/platform/convex/agent_tools/database/helpers/schema_definitions.tsservices/platform/convex/agent_tools/documents/internal_actions.tsservices/platform/convex/agent_tools/documents/internal_mutations.tsservices/platform/convex/agent_tools/human_input/internal_mutations.tsservices/platform/convex/agent_tools/human_input/mutations.tsservices/platform/convex/agent_tools/human_input/request_human_input_tool.tsservices/platform/convex/agent_tools/integrations/internal_actions.tsservices/platform/convex/agent_tools/integrations/internal_mutations.tsservices/platform/convex/agent_tools/integrations/verify_approval_tool.tsservices/platform/convex/agent_tools/workflows/__tests__/execute_approved_workflow_run.test.tsservices/platform/convex/agent_tools/workflows/__tests__/execute_approved_workflow_update.test.tsservices/platform/convex/agent_tools/workflows/internal_actions.tsservices/platform/convex/agent_tools/workflows/internal_mutations.tsservices/platform/convex/agent_tools/workflows/update_workflow_step_tool.tsservices/platform/convex/approvals/helpers.tsservices/platform/convex/approvals/internal_queries.tsservices/platform/convex/approvals/list_approvals_paginated.test.tsservices/platform/convex/approvals/mutations.tsservices/platform/convex/approvals/queries.tsservices/platform/convex/approvals/schema.tsservices/platform/convex/approvals/types.tsservices/platform/convex/approvals/validators.tsservices/platform/convex/conversations/send_message_via_integration.tsservices/platform/convex/lib/agent_chat/__tests__/start_agent_chat.test.tsservices/platform/convex/lib/agent_chat/start_agent_chat.tsservices/platform/convex/lib/create_agent_config.tsservices/platform/convex/lib/variables/replace_variables_in_string.tsservices/platform/convex/wf_executions/mutations.tsservices/platform/convex/wf_executions/queries.tsservices/platform/convex/wf_step_defs/internal_mutations.tsservices/platform/convex/workflow_engine/action_defs/approval/helpers/types.tsservices/platform/convex/workflow_engine/action_defs/conversation/conversation_action.tsservices/platform/convex/workflow_engine/action_defs/workflow_processing_records/workflow_processing_records_action.tsservices/platform/convex/workflow_engine/helpers/engine/dynamic_workflow_handler.tsservices/platform/convex/workflow_engine/helpers/engine/execute_step_handler.tsservices/platform/convex/workflow_engine/helpers/engine/on_workflow_complete.test.tsservices/platform/convex/workflow_engine/helpers/engine/on_workflow_complete.tsservices/platform/convex/workflow_engine/helpers/nodes/llm/execute_agent_with_tools.tsservices/platform/convex/workflow_engine/helpers/nodes/llm/execute_llm_node.tsservices/platform/convex/workflow_engine/helpers/nodes/llm/utils/build_human_input_context.tsservices/platform/convex/workflow_engine/helpers/validation/variables/parse.tsservices/platform/convex/workflow_engine/internal_actions.tsservices/platform/convex/workflows/executions/update_execution_status.tsservices/platform/messages/en.json
💤 Files with no reviewable changes (1)
- services/platform/convex/lib/agent_chat/tests/start_agent_chat.test.ts
| // Position-based skip: find the cursor message by ID in the asc-ordered | ||
| // results and take only messages after it. This avoids timestamp precision | ||
| // issues and correctly handles multiple messages within the same second. | ||
| // Then trim to the originally requested $top so the caller gets exactly | ||
| // the number of messages it asked for. | ||
| if (cursor?.messageId) { | ||
| const cursorIndex = rawValues.findIndex(function (msg) { | ||
| return msg.id === cursor.messageId; | ||
| }); | ||
| if (cursorIndex !== -1) { | ||
| rawValues = rawValues.slice( | ||
| cursorIndex + 1, | ||
| cursorIndex + 1 + requestedTop, | ||
| ); | ||
| } | ||
| // If cursor message not found (deleted or outside window), keep all results | ||
| } |
There was a problem hiding this comment.
Comment inaccuracy and missing trim when cursor not found.
Two issues:
-
The comment on line 386 says "asc-ordered results" but the default orderby is
receivedDateTime desc(line 314). The logic is correct, but the comment is misleading. -
When the cursor message is not found (line 401 fallback),
rawValuesretains up torequestedTop + 10messages from the over-fetch. Callers may receive more messages than requested, which could cause unexpected behavior.
Proposed fix
// Position-based skip: find the cursor message by ID in the
- // asc-ordered results and take only messages after it. This avoids
+ // results and take only messages after it. This avoids
// timestamp precision issues and correctly handles multiple messages
// within the same second. Then trim to the originally requested $top
// so the caller gets exactly the number of messages it asked for.
if (cursor?.messageId) {
const cursorIndex = rawValues.findIndex(function (msg) {
return msg.id === cursor.messageId;
});
if (cursorIndex !== -1) {
rawValues = rawValues.slice(
cursorIndex + 1,
cursorIndex + 1 + requestedTop,
);
+ } else {
+ // Cursor message not found (deleted or outside window) — trim to
+ // requested count to avoid returning more messages than expected.
+ rawValues = rawValues.slice(0, requestedTop);
}
- // If cursor message not found (deleted or outside window), keep all results
}📝 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.
| // Position-based skip: find the cursor message by ID in the asc-ordered | |
| // results and take only messages after it. This avoids timestamp precision | |
| // issues and correctly handles multiple messages within the same second. | |
| // Then trim to the originally requested $top so the caller gets exactly | |
| // the number of messages it asked for. | |
| if (cursor?.messageId) { | |
| const cursorIndex = rawValues.findIndex(function (msg) { | |
| return msg.id === cursor.messageId; | |
| }); | |
| if (cursorIndex !== -1) { | |
| rawValues = rawValues.slice( | |
| cursorIndex + 1, | |
| cursorIndex + 1 + requestedTop, | |
| ); | |
| } | |
| // If cursor message not found (deleted or outside window), keep all results | |
| } | |
| // Position-based skip: find the cursor message by ID in the | |
| // results and take only messages after it. This avoids | |
| // timestamp precision issues and correctly handles multiple messages | |
| // within the same second. Then trim to the originally requested $top | |
| // so the caller gets exactly the number of messages it asked for. | |
| if (cursor?.messageId) { | |
| const cursorIndex = rawValues.findIndex(function (msg) { | |
| return msg.id === cursor.messageId; | |
| }); | |
| if (cursorIndex !== -1) { | |
| rawValues = rawValues.slice( | |
| cursorIndex + 1, | |
| cursorIndex + 1 + requestedTop, | |
| ); | |
| } else { | |
| // Cursor message not found (deleted or outside window) — trim to | |
| // requested count to avoid returning more messages than expected. | |
| rawValues = rawValues.slice(0, requestedTop); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/integrations/outlook/connector.ts` around lines 386 - 402, Update
the misleading comment to reflect that results are fetched in descending order
by receivedDateTime (orderby: receivedDateTime desc) and ensure we always trim
any over-fetched results down to requestedTop; specifically, after the cursor
handling in the block that references cursor?.messageId, keep the existing
findIndex/slice behavior when the cursor is found, and add a fallback that sets
rawValues = rawValues.slice(0, requestedTop) when the cursor message is not
found (so rawValues never contains more than requestedTop); refer to the
variables cursor, cursor.messageId, rawValues, and requestedTop when making
these changes.
| const shouldShow = | ||
| message.role === 'user' || | ||
| message.content !== '' || | ||
| message.isAborted; | ||
|
|
||
| return shouldShow ? ( | ||
| <MessageBubble | ||
| key={message.key} | ||
| message={{ | ||
| ...message, | ||
| role: message.role === 'user' ? 'user' : 'assistant', | ||
| threadId: threadId, | ||
| }} | ||
| onSendFollowUp={onSendFollowUp} | ||
| /> | ||
| ) : null; |
There was a problem hiding this comment.
Don't hide assistant messages that only contain files.
shouldShow only checks content and isAborted, so an assistant message with empty text but populated attachments or fileParts now returns null and never reaches MessageBubble. Include file payload presence in the visibility check before filtering it out.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/app/features/chat/components/chat-messages.tsx` around
lines 172 - 187, The visibility predicate for messages (shouldShow) currently
only checks message.role, message.content, and message.isAborted, which causes
assistant messages that have empty content but non-empty file payloads to be
hidden; update the shouldShow logic used before rendering MessageBubble to also
treat messages with message.attachments.length > 0 or message.fileParts.length >
0 as visible so assistant messages containing files reach the MessageBubble
component (preserving existing role normalization and threadId injection).
| await updateApprovalStatus({ | ||
| approvalId, | ||
| status: 'approved', | ||
| status: 'executing', | ||
| }); | ||
| await executeDocumentWrite({ approvalId }); | ||
| } catch (err) { |
There was a problem hiding this comment.
Make the approve transition and execution start atomic.
Line 78 persists executing before Line 82 starts the write in a second request. If that second call fails or the tab disconnects in between, the approval is stuck in executing and the user loses the retry/reject path. Move the status transition into executeDocumentWrite, or add a single claim-and-execute backend entrypoint.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@services/platform/app/features/chat/components/document-write-approval-card.tsx`
around lines 78 - 83, The current flow calls updateApprovalStatus({ approvalId,
status: 'executing' }) and then separately calls executeDocumentWrite({
approvalId }), which can leave the approval stuck in 'executing' if
executeDocumentWrite fails or the client disconnects; make the transition atomic
by moving the status update into the executeDocumentWrite backend path or by
introducing a single server-side claim-and-execute endpoint (e.g., combine
updateApprovalStatus and execution inside executeDocumentWrite or create
executeAndClaimApproval(approvalId)). Update the client to call only the new
atomic endpoint (remove the separate updateApprovalStatus call) and ensure the
backend performs a transactional claim (set status to executing), runs the
write, and updates final status or reverts on failure so retries/rejects remain
possible.
| onSuccess: () => { | ||
| // Defer clear so the Convex subscription hides the form first, | ||
| // then clear state invisibly for the next human input request | ||
| setTimeout(() => { | ||
| setHumanInputValue(''); | ||
| setHumanInputSelected(''); | ||
| setHumanInputMulti([]); | ||
| }, 500); | ||
| }, |
There was a problem hiding this comment.
Don't let the previous response timeout wipe the next prompt.
The delayed clear isn't tied to the current approval ID. If the workflow asks a follow-up question within 500 ms, this callback will fire against the new request and clear whatever the user has already typed or selected. Cancel the timeout on approval change/unmount, or only clear when the same approval is still active.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@services/platform/app/features/chat/components/workflow-run-approval-card.tsx`
around lines 287 - 295, The delayed clear in the onSuccess callback of
workflow-run-approval-card.tsx uses setTimeout without cancellation or
validation, so if a new approval appears within 500ms it will wipe the new
input; fix by capturing the timeout id and cancelling it when the active
approval changes or the component unmounts (use an effect cleanup), or instead
check the current approval identifier (e.g., approval.id / activeApprovalId)
inside the timeout and only call setHumanInputValue / setHumanInputSelected /
setHumanInputMulti if the approval id still matches the one that scheduled the
timeout.
| {metadata.updateType === 'full_save' | ||
| ? `${metadata.stepsConfig?.length ?? 0} steps` | ||
| : `Step: ${metadata.stepName ?? 'unknown'}`} | ||
| : metadata.updateType === 'multi_step_patch' | ||
| ? `${metadata.steps?.length ?? 0} steps` | ||
| : `Step: ${metadata.stepName ?? 'unknown'}`} |
There was a problem hiding this comment.
Localize the disclosure summary strings too.
This branch still renders raw English ("steps", Step:, unknown), so the card remains partially untranslated in non-English locales. Move these labels into workflowUpdateApproval like the rest of the component.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@services/platform/app/features/chat/components/workflow-update-approval-card.tsx`
around lines 232 - 236, The disclosure summary currently outputs hardcoded
English labels ("steps", "Step:", "unknown") when rendering based on
metadata.updateType; update the rendering in the component that uses
metadata.updateType / metadata.stepsConfig / metadata.steps / metadata.stepName
to instead use localized strings from the existing workflowUpdateApproval i18n
object (e.g., workflowUpdateApproval.stepsLabel,
workflowUpdateApproval.stepLabel, workflowUpdateApproval.unknownLabel),
replacing the raw literals so all branches use translated text while preserving
the numeric fallback logic.
| // Write system message to thread so the AI knows this was user-initiated | ||
| const triggerData = isRecord(execution.triggerData) | ||
| ? execution.triggerData | ||
| : null; | ||
| const approvalIdStr = triggerData | ||
| ? getString(triggerData, 'approvalId') | ||
| : undefined; | ||
| if (approvalIdStr) { | ||
| try { | ||
| const approval = await ctx.db.get(toId<'approvals'>(approvalIdStr)); | ||
| if (approval?.threadId) { | ||
| await saveMessage(ctx, components.agent, { | ||
| threadId: approval.threadId, | ||
| message: { | ||
| role: 'system', | ||
| content: `[WORKFLOW_CANCELLED]\nWorkflow "${execution.workflowSlug ?? 'unknown'}" was stopped.`, | ||
| }, | ||
| }); | ||
| } | ||
| } catch { | ||
| // Non-critical: system message failure should not block cancellation | ||
| } | ||
| } |
There was a problem hiding this comment.
Don't route cancellation messages from triggerData.
Line 101 trusts execution.triggerData.approvalId to choose the destination thread. triggerData is caller-supplied on startWorkflow, so a spoofed or stale approval ID can make cancellation write [WORKFLOW_CANCELLED] into an unrelated approval thread. Resolve the thread from trusted execution data, or verify that the loaded approval actually belongs to this execution/workflow before calling saveMessage.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/convex/wf_executions/mutations.ts` around lines 97 - 119,
The code trusts execution.triggerData.approvalId to pick the thread to notify,
which allows caller-supplied triggerData to route cancellation messages;
instead, load the approval via ctx.db.get(toId<'approvals'>(approvalIdStr)) only
as a candidate and verify it is actually associated with this execution before
calling saveMessage (e.g., compare approval.executionId or
approval.workflowRunId/approval.workflowSlug against this execution.id or
execution.workflowSlug), and skip saveMessage if the approval does not belong to
the current execution; keep using components.agent and the same message payload
but only after the ownership check to prevent spoofed/stale IDs from writing to
unrelated threads.
| import { createDebugLog } from '../../../lib/debug_log'; | ||
| import { | ||
| jsonRecordValidator, | ||
| jsonValueValidator, | ||
| } from '../../../lib/shared/schemas/utils/json_value'; | ||
|
|
||
| const debugLog = createDebugLog('DEBUG_WORKFLOW', '[Conversations]'); | ||
| import { createConversation } from './helpers/create_conversation'; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Import ordering: move const debugLog declaration after all imports.
The debugLog constant declaration appears between imports, which breaks the expected file structure where all imports precede code declarations. Move the createDebugLog call after the last import statement.
♻️ Proposed fix
import { createDebugLog } from '../../../lib/debug_log';
import {
jsonRecordValidator,
jsonValueValidator,
} from '../../../lib/shared/schemas/utils/json_value';
-
-const debugLog = createDebugLog('DEBUG_WORKFLOW', '[Conversations]');
import { createConversation } from './helpers/create_conversation';
import { createConversationFromEmail } from './helpers/create_conversation_from_email';
import { createConversationFromSentEmail } from './helpers/create_conversation_from_sent_email';
...
import { updateConversations } from './helpers/update_conversations';
+
+const debugLog = createDebugLog('DEBUG_WORKFLOW', '[Conversations]');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@services/platform/convex/workflow_engine/action_defs/conversation/conversation_action.ts`
around lines 19 - 26, The file places the const debugLog =
createDebugLog('DEBUG_WORKFLOW', '[Conversations]') between import statements;
move that declaration so all imports come first (i.e., after the import block
containing createConversation and the json validators). Specifically, remove the
inline call between imports and re-add const debugLog = createDebugLog(...)
immediately after the last import (references: createDebugLog, debugLog,
createConversation, jsonRecordValidator, jsonValueValidator) so the file has
only imports up top followed by the debugLog declaration.
| const pendingApprovals = await ctx.runQuery( | ||
| internal.approvals.internal_queries.listPendingForExecution, | ||
| { executionId: toId<'wfExecutions'>(args.executionId) }, | ||
| ); | ||
| const humanInputApproval = pendingApprovals.find( | ||
| (a: { resourceType: string; stepSlug?: string }) => | ||
| a.resourceType === 'human_input_request' && | ||
| a.stepSlug === args.stepSlug, | ||
| ); | ||
| if (humanInputApproval) { | ||
| approvalTaskId = humanInputApproval._id; | ||
| } |
There was a problem hiding this comment.
Select the new human-input approval deterministically.
If a retry or duplicate request_human_input call leaves more than one pending approval for this stepSlug, .find() on the execution-wide list can pick an older record. The workflow will then wait on the wrong approval ID. Prefer the newest matching approval here, or better, return the created approval ID directly from the tool path and avoid this scan entirely.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@services/platform/convex/workflow_engine/helpers/engine/execute_step_handler.ts`
around lines 212 - 223, The current logic uses pendingApprovals.find(...) which
can pick an older approval if duplicates exist; update the selection to
deterministically choose the newest matching human-input approval by filtering
pendingApprovals for resourceType === 'human_input_request' and stepSlug ===
args.stepSlug, then pick the one with the latest created timestamp (or largest
_id if _id encodes time) and assign its _id to approvalTaskId; alternatively
(preferred) change the tool path that issues request_human_input to return the
created approval ID directly and use that value instead of scanning
pendingApprovals.
| const escapeForContext = (value: string) => | ||
| value.replace(/</g, '<').replace(/>/g, '>'); | ||
|
|
||
| const formatResponse = (response: string | string[]) => | ||
| Array.isArray(response) | ||
| ? response.map(escapeForContext).join(', ') | ||
| : escapeForContext(response); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider escaping additional characters for prompt safety.
The escapeForContext function escapes < and > to prevent XML/HTML injection within the <human_input_context> tags. However, user-provided questions/responses may contain:
- Newlines (
\n) that could break the- Q: "..." → A: "..."format - Quotes (
") that could prematurely close the quoted strings
Consider escaping or replacing these to ensure the context block remains well-formed:
♻️ Suggested enhancement
const escapeForContext = (value: string) =>
- value.replace(/</g, '<').replace(/>/g, '>');
+ value
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/"/g, '"')
+ .replace(/\n/g, ' ');📝 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.
| const escapeForContext = (value: string) => | |
| value.replace(/</g, '<').replace(/>/g, '>'); | |
| const formatResponse = (response: string | string[]) => | |
| Array.isArray(response) | |
| ? response.map(escapeForContext).join(', ') | |
| : escapeForContext(response); | |
| const escapeForContext = (value: string) => | |
| value | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>') | |
| .replace(/"/g, '"') | |
| .replace(/\n/g, ' '); | |
| const formatResponse = (response: string | string[]) => | |
| Array.isArray(response) | |
| ? response.map(escapeForContext).join(', ') | |
| : escapeForContext(response); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@services/platform/convex/workflow_engine/helpers/nodes/llm/utils/build_human_input_context.ts`
around lines 29 - 35, escapeForContext only converts < and >; extend it to also
replace newline characters with the literal "\n" (so the Q/A lines remain
single-line) and escape double quotes (e.g., replace " with " or \" ) so
quoted strings in the prompt cannot break formatting; update escapeForContext
and ensure formatResponse continues to call it (symbols: escapeForContext,
formatResponse, build_human_input_context) so all user questions/responses are
normalized before being inserted into the <human_input_context> block.
| "workflowRunApproval": { | ||
| "approve": "Run workflow", | ||
| "reject": "Cancel", | ||
| "approveTooltip": "Approve and run this workflow", | ||
| "rejectTooltip": "Cancel workflow execution", | ||
| "statusApprovedSuccess": "Workflow execution started successfully.", | ||
| "statusApprovedFailed": "Workflow execution was approved but failed to start.", | ||
| "statusExecuting": "Executing...", | ||
| "statusCompleted": "Completed", | ||
| "statusCompletedSuccess": "Workflow execution started successfully.", | ||
| "statusCompletedFailed": "Workflow execution was approved but failed to start.", | ||
| "statusRejected": "Rejected", |
There was a problem hiding this comment.
Use the same cancellation wording for workflow runs.
This block now uses “Cancel workflow execution” in the tooltip, but the terminal status still reads “Rejected”. Once these status messages are shown in chat, that inconsistency becomes visible to users; it should match the “cancelled” wording used by the creation/update approval cards.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/messages/en.json` around lines 2155 - 2163, Update the
wording under the workflowRunApproval block so the rejection/cancellation terms
match: change the status key value for "statusRejected" to use the same
"Cancelled" wording as "rejectTooltip" ("Cancel workflow execution") — locate
the workflowRunApproval JSON object and update the "statusRejected" string to
"Cancelled" (or the exact cancelled phrasing used by creation/update approval
cards) to keep tooltip and terminal status consistent.
…Outlook pagination Implement end-to-end human input request flow for workflow executions, allowing LLM agents to pause and request user input via approval cards. Adds approval queries, mutation handlers, and chat UI components for reviewing and responding to input requests. Also fixes Outlook connector cursor-based pagination with position-based skip to handle sub-second timestamp precision issues.
… input loop
Replace the single 'approved' status with 'executing' and 'completed' to accurately track approval lifecycle — approvals now transition pending→executing→completed/rejected, with the triggering approval auto-updated on workflow completion.
Key fixes:
- Re-execute the same LLM step after human input resume instead of advancing to next step
- Inject humanInputContext as a template variable (not appended to system prompt) so {{humanInputContext}} placeholders in workflow configs resolve correctly
- Write rejection reason as system message to thread so the AI acknowledges user-initiated rejections
- Clear waitingFor with empty string instead of undefined (Convex strips undefined from serialized args)
- Update all approval card UIs for new status values with executing=blue/processing badge
- Add request_human_input tool and {{humanInputContext}} to contract generation workflow
…and LLM input sanitization Adds batch update support to the workflow step tool with JSON sanitization for malformed LLM output. Extracts ApprovalCardRenderer component and refactors chat to show a single active approval at the bottom. Adds human input context builder for LLM nodes and new approval helper queries.
Show all system messages (approval rejections, workflow cancellations, etc.) in chat with a compact, expandable UI instead of filtering them out. Human input response messages continue to render as user bubbles.
…atomic execution claims Extract hardcoded strings into translation keys across all approval and human input cards with a shared approvalCommon namespace. Add ARIA roles and attributes to message lists and interactive form elements. Replace read-then-check idempotency guards with an atomic claimWorkflowApprovalForExecution mutation to prevent race conditions. Fix approval queries to only swallow UnauthorizedError instead of all exceptions. Escape HTML entities in human input context to prevent LLM injection.
…m message formatting Remove executedAt early-return checks from approval update mutations since the atomic claim pattern now prevents double execution. Drop the server-side concurrent generation guard in startAgentChat to avoid permanently deadlocking threads when generation crashes. Format system message tags for readability.
… clean up system messages
…ion claim Remove unused tCommon import, replace role="checkbox" with aria-pressed on toggle buttons, and update test mocks to handle claimWorkflowApprovalForExecution.
7cccc26 to
142a72b
Compare
Summary
executing→completedlifecycle with atomic claim mutations to prevent race conditionshumanInputContextas a template variableTest plan
use-merged-chat-items,use-message-processing,execute_approved_workflow_run,on_workflow_completeSummary by CodeRabbit
New Features
Improvements