From edc44e63961a49fb22f109e3d31cf8d90030b85a Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 3 Sep 2026 14:08:32 -0700 Subject: [PATCH 1/2] fix: address review findings from the v0.8.21 release PR Five findings from the release review that held up on inspection: - The member-connector listing is workspace-scoped but used the concealing knowledge-base error policy, so a caller without workspace access was told a knowledge base they never named could not be found. Give it a workspace-scoped policy, the way the bulk routes already have one. - Restoring a queued Build message left the search query live, and a live query puts the composer straight back into Search, so the edit was stranded. Clear it on restore through the existing guarded helper. - The self-hosting Redis page said losing the instance cannot affect completed work. High-volume webhook triggers claim idempotency keys in Redis by default, so a restart can let a provider's retry run a finished webhook a second time. Document that, and the database-backed escape hatch. - /ship gained a docs-manifest check without a matching regenerate step, so a PR that adds, removes, or renames a docs page aborted instead of self-healing. - The CLI updater prints a Yarn upgrade command, but the manual upgrade instructions offered only npm, pnpm, and bun. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_013e5sXbYST2R4qNzM996GFg --- .agents/skills/ship/SKILL.md | 4 ++-- .../docs/content/docs/cli/troubleshooting.mdx | 7 +++++- .../docs/platform/self-hosting/redis.mdx | 4 +++- .../api/knowledge/member-connectors/route.ts | 2 +- .../app/workspace/[workspaceId]/home/home.tsx | 17 ++++++++------ .../lib/knowledge/api/route-policies.test.ts | 23 ++++++++++++++++++- apps/sim/lib/knowledge/api/route-policies.ts | 2 ++ 7 files changed, 46 insertions(+), 13 deletions(-) diff --git a/.agents/skills/ship/SKILL.md b/.agents/skills/ship/SKILL.md index d625e72c2ec..e465ccd7672 100644 --- a/.agents/skills/ship/SKILL.md +++ b/.agents/skills/ship/SKILL.md @@ -37,10 +37,10 @@ When the user runs `/ship`: - `bun run check:migrations origin/staging` must pass (staging is the PR base). Do not silence a flagged statement with a `-- migration-safe:` annotation unless `/db-migrate` confirmed the old code no longer depends on it; otherwise split the destructive change into a later deploy. 6. **Run pre-ship checks** from the repo root before staging. This has two phases: first **regenerate** every committed artifact so generated files never drift into a CI failure (this is what catches things like `agent-stream-docs` going stale after a `models.ts` edit), then run the **full audit suite** CI's `Lint and Test` job enforces. Both phases parallelize — but only across commands that write **disjoint** outputs — and a bare `wait` swallows child exit codes, so both phases below explicitly collect each job's status and abort ship if any failed. - **Phase A — regenerate the always-in-repo committed artifacts (parallel), then let step 7 stage whatever changed.** Regenerate only the generators whose inputs live entirely in this repo and that any ordinary code change can drift — `agent-stream-docs:generate` (derives from the provider model registry) and `skills:sync` (derives from `.agents/skills/**`). They write disjoint trees (`apps/docs/…/agent.mdx` vs the `.claude`/`.cursor` command projections), so they parallelize safely, and each is idempotent (a no-op when already in sync): + **Phase A — regenerate the always-in-repo committed artifacts (parallel), then let step 7 stage whatever changed.** Regenerate only the generators whose inputs live entirely in this repo and that any ordinary code change can drift — `agent-stream-docs:generate` (derives from the provider model registry), `skills:sync` (derives from `.agents/skills/**`), and `docs-manifest:generate` (derives from the `apps/docs/content/docs` page tree, so adding, removing, or renaming a page drifts it). They write disjoint files (`apps/docs/…/agent.mdx`, the `.claude`/`.cursor` command projections, and `apps/sim/lib/copilot/generated/docs-manifest.ts`), so they parallelize safely, and each is idempotent (a no-op when already in sync): ```bash rm -f /tmp/ship-gen-results - for g in agent-stream-docs:generate skills:sync; do + for g in agent-stream-docs:generate skills:sync docs-manifest:generate; do ( bun run "$g" >"/tmp/ship-gen-${g//:/-}.log" 2>&1; echo "$? $g" >>/tmp/ship-gen-results ) & done wait diff --git a/apps/docs/content/docs/cli/troubleshooting.mdx b/apps/docs/content/docs/cli/troubleshooting.mdx index b429a455b4a..503005a8cd0 100644 --- a/apps/docs/content/docs/cli/troubleshooting.mdx +++ b/apps/docs/content/docs/cli/troubleshooting.mdx @@ -100,7 +100,7 @@ sim --version Then upgrade with the package manager you installed it with — using a different one installs a second copy instead of replacing the executable on your `PATH`: - + ```bash npm install -g sim@latest @@ -116,6 +116,11 @@ one installs a second copy instead of replacing the executable on your `PATH`: bun add -g sim@latest ``` + + ```bash + yarn global add sim@latest + ``` + The CLI can also tell you this through a cached daily check on eligible diff --git a/apps/docs/content/docs/platform/self-hosting/redis.mdx b/apps/docs/content/docs/platform/self-hosting/redis.mdx index 45170c0511f..ee9e64d4f76 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 claim in PostgreSQL instead, which fate-shares the claim with the transaction; see `forceStorage: 'database'` on the idempotency service. 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 From e57d8545f8fd66b83565f7ac214f09d4aa8e8591 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 3 Sep 2026 14:28:38 -0700 Subject: [PATCH 2/2] fix(docs): separate durable idempotency storage from transactional coupling The Redis page sent a reader whose side effect must not replay to `forceStorage: 'database'`. That keeps the claim in PostgreSQL so a restart cannot drop it, but the service still claims, runs, and records the result in separate steps, so a side effect that commits before the result is recorded can still replay. `executeTransactionallyIdempotent` is the one that commits the claim, the mutation, and the result together. Name both and say which solves which problem. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_013e5sXbYST2R4qNzM996GFg --- apps/docs/content/docs/platform/self-hosting/redis.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/docs/platform/self-hosting/redis.mdx b/apps/docs/content/docs/platform/self-hosting/redis.mdx index ee9e64d4f76..ba704630299 100644 --- a/apps/docs/content/docs/platform/self-hosting/redis.mdx +++ b/apps/docs/content/docs/platform/self-hosting/redis.mdx @@ -26,7 +26,7 @@ Sim uses Redis as a message bus and shared cache. Both deployments ship it by de 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 claim in PostgreSQL instead, which fate-shares the claim with the transaction; see `forceStorage: 'database'` on the idempotency service. Enabling Redis persistence narrows the window but does not close it. +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