diff --git a/apps/docs/content/docs/platform/self-hosting/redis.mdx b/apps/docs/content/docs/platform/self-hosting/redis.mdx index 45170c0511f..ba704630299 100644 --- a/apps/docs/content/docs/platform/self-hosting/redis.mdx +++ b/apps/docs/content/docs/platform/self-hosting/redis.mdx @@ -24,7 +24,9 @@ Sim uses Redis as a message bus and shared cache. Both deployments ship it by de With more than one app or realtime replica and no `REDIS_URL`, users on different pods stop seeing each other's edits and live status updates. Beyond one startup log line noting single-pod mode, nothing is logged — the app looks healthy and quietly loses events. Treat Redis as mandatory the moment `replicaCount` exceeds 1. -Everything Sim keeps in Redis is cache, coordination state, or an in-flight event — never committed data, which lives in PostgreSQL and object storage. Persistence is therefore not required. Losing or restarting the instance is not free, though: cancellation markers and the cross-pod half of execution streaming live here, so active runs stop streaming and a cancellation issued across the gap may not land. Completed work is unaffected. +Everything Sim keeps in Redis is cache, coordination state, or an in-flight event — never committed data, which lives in PostgreSQL and object storage. Persistence is therefore not required. Losing or restarting the instance is not free, though: cancellation markers and the cross-pod half of execution streaming live here, so active runs stop streaming and a cancellation issued across the gap may not land. + +One exception is worth planning for. High-volume webhook triggers claim their idempotency keys in Redis by default, trading a narrow durability window for the throughput. A restart drops those claims, so a provider that retries a webhook Sim already finished can have it execute a second time. Flows where a repeat is not acceptable — anything touching money, billing, or compliance — should not rest on that claim. `forceStorage: 'database'` on the idempotency service keeps the claim in PostgreSQL so a restart cannot drop it, but it still claims, runs, and records the result in separate steps, so a side effect that commits before the result is recorded can replay. Where the side effect itself must never run twice, `executeTransactionallyIdempotent` commits the claim, the mutation, and the result together in the caller's transaction. Enabling Redis persistence narrows the window but does not close it. ## Configuration diff --git a/apps/sim/app/api/knowledge/member-connectors/route.ts b/apps/sim/app/api/knowledge/member-connectors/route.ts index 4467c3ec35e..1c9db2a0230 100644 --- a/apps/sim/app/api/knowledge/member-connectors/route.ts +++ b/apps/sim/app/api/knowledge/member-connectors/route.ts @@ -13,7 +13,7 @@ export const GET = defineInternalJsonRoute({ auth: internalSessionAuth, operation: knowledgeOperations.listWorkspaceMemberConnectors, rateLimit: internalRateLimits.none({ reason: 'One small read per visit to the Search tab' }), - errorPolicy: internalKnowledgeErrorPolicies.connectors, + errorPolicy: internalKnowledgeErrorPolicies.memberConnectors, mapInput: ({ query }) => ({ workspaceId: query.workspaceId }), useCase: listWorkspaceMemberConnectors, present: ({ connectors }) => ({ success: true as const, data: connectors }), diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index 014e0058998..c5560e1ba0d 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -543,23 +543,26 @@ export function Home({ chatId, userName, userId }: HomeProps) { ] ) + /** An emptied search box returns to the sources; a send in any other mode has no search to clear. */ + const clearSearch = useCallback(() => { + if (searchQueryValue !== null) setSearchQuery('') + }, [searchQueryValue, setSearchQuery]) + /** * A queued message re-enters the composer in the mode it was written in: an * Assistant question edits as an Assistant question, and never as a Search, - * which submits nothing and would leave the edit stranded. + * which submits nothing and would leave the edit stranded. Leaving Search + * drops the query as it does everywhere else; a live one would put the + * composer straight back into Search and strand the edit anyway. */ const restoreQueuedMode = useCallback( (requestMode: QueuedMessage['requestMode']) => { + clearSearch() void setComposerMode(requestMode === 'ask' ? 'assistant' : 'build') }, - [setComposerMode] + [clearSearch, setComposerMode] ) - /** An emptied search box returns to the sources; a send in any other mode has no search to clear. */ - const clearSearch = useCallback(() => { - if (searchQueryValue !== null) setSearchQuery('') - }, [searchQueryValue, setSearchQuery]) - /** * Summarize or Answer on a result: switch to Assistant and hand the question * to it. The submit reads the mode from this render, so it is sent as an diff --git a/apps/sim/lib/knowledge/api/route-policies.test.ts b/apps/sim/lib/knowledge/api/route-policies.test.ts index 25a4c789890..7eab1ce7477 100644 --- a/apps/sim/lib/knowledge/api/route-policies.test.ts +++ b/apps/sim/lib/knowledge/api/route-policies.test.ts @@ -12,7 +12,10 @@ import { WorkspaceApiKeyScopeAuthorizationError, } from '@/lib/core/application' import { OrchestrationError } from '@/lib/core/orchestration/types' -import { v2KnowledgeErrorPolicies } from '@/lib/knowledge/api/route-policies' +import { + internalKnowledgeErrorPolicies, + v2KnowledgeErrorPolicies, +} from '@/lib/knowledge/api/route-policies' describe('v2 knowledge error policies', () => { it.each([ @@ -70,3 +73,21 @@ describe('v2 knowledge error policies', () => { }) }) }) + +describe('internal knowledge error policies', () => { + it('conceals a knowledge-base-scoped connector authorization failure', () => { + const projected = internalKnowledgeErrorPolicies.connectors.project( + new NoWorkspaceAccessError() + ) + expect(projected?.status).toBe(404) + expect(projected?.body).toMatchObject({ error: 'Knowledge base not found' }) + }) + + it('does not conceal the workspace-scoped member-connector listing', () => { + const projected = internalKnowledgeErrorPolicies.memberConnectors.project( + new NoWorkspaceAccessError() + ) + expect(projected?.status).not.toBe(404) + expect(JSON.stringify(projected?.body)).not.toContain('Knowledge base not found') + }) +}) diff --git a/apps/sim/lib/knowledge/api/route-policies.ts b/apps/sim/lib/knowledge/api/route-policies.ts index 291585938dd..9f59594f5e2 100644 --- a/apps/sim/lib/knowledge/api/route-policies.ts +++ b/apps/sim/lib/knowledge/api/route-policies.ts @@ -110,6 +110,8 @@ export const internalKnowledgeErrorPolicies = { internalKnowledgeErrorPolicy('Failed to process knowledge tag request') ), connectors: concealKnowledgeBase(internalKnowledgeErrorPolicy('Internal server error')), + /** Workspace-scoped like the bulk routes above, so likewise not concealed. */ + memberConnectors: internalKnowledgeErrorPolicy('Failed to fetch member connectors'), uploads: concealKnowledgeBase(internalKnowledgeUploadErrorPolicy), } as const