59eead238ddecd2be34094cd25686a71cbefa0ff
11500 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
59eead238d |
message list member backfill (#23176)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23176?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
97d2b52a71 |
fix(front): duplicate junction chip after Add New in relation picker (#23185)
Clicking Add New in a junction relation picker rendered the newly created target twice until a page reload. The handler appended the created junction to the source record's store field after awaiting the mutation, but useCreateOneRecord's post-optimistic effect had already attached it, so the same junction id ended up in the field array twice (single row in DB). Removes the redundant manual append in both to-many picker flows. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23185?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
6623901eb4 |
chore: bump version to 2.25.0 (#23221)
## Summary - Moves current version to previous versions array - Sets TWENTY_CURRENT_VERSION to the new version - Updates TWENTY_NEXT_VERSIONS with the next minor version - Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same version ## Checklist - [ ] Verify version constants are correct - [ ] Verify npm package versions match <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23221?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com> |
||
|
|
b91c2a6457 |
fix(server): repair missing applicationRegistration.logoFileId on upgraded instances (#23215)
## Problem closes https://github.com/twentyhq/twenty/issues/23210 Self-hosted instances on 2.23.x fail their workspace upgrade with: ``` column ApplicationEntity__ApplicationEntity_applicationRegistration.logoFileId does not exist at UpgradePeopleDataLabsApplicationCommand.runOnWorkspace ``` The `2-21` instance command that adds `core."applicationRegistration"."logoFileId"` was merged ~20 minutes after the 2.22 version bump (PR #22827, `94192a2164`), so it first shipped in 2.22 while registered under `@RegisteredInstanceCommand('2.21.0', ...)`. The upgrade runner resolves its start position from the last recorded command and only moves forward. Any instance that had already run a 2.21.x binary has its cursor past that slot, so the command is skipped permanently and the column is never created. `UpgradeAwareEntityMetadataAdapter` decides column visibility positionally (`index < currentCursor`), not by whether the command actually ran, so it keeps `logoFileId` in the SELECT list and the instance reports "Up to date" while the column is absent. **Affected:** instances that ran 2.21.x, then upgraded to >= 2.22. Instances that went from <= 2.20 straight to >= 2.22 replayed the full sequence and are fine. `logoFileId` is populated lazily by design (NULL is a supported state), so no backfill is added. ## Changes **1. Idempotent DDL guard in the failing workspace command** `2-23-workspace-command-...-upgrade-people-data-labs-application.command.ts` now ensures the column exists at the top of `runOnWorkspace`, before the `findOne` that crashes on affected instances. It uses the core `DataSource` (`@InjectDataSource()`) because `core."applicationRegistration"` is instance-global, guards with a per-process boolean in addition to the SQL-level `IF NOT EXISTS`, and copies the full statement list (column + unique + FK constraints) verbatim from the 2.21 command. In dry-run it probes `information_schema.columns` and returns instead of running the crashing query. **2. Fast instance command in 2.23** New `2-23-instance-command-fast-1784823473532-add-logo-file-id-to-application-registration.ts`, registered at the end of the 2.23 fast segment (highest timestamp), running the same idempotent DDL. This covers the normal 2.22 -> 2.23 path and, critically, instances with zero provisioned workspaces where the workspace command body never executes. The shared DDL lives in `2-23/utils/ensure-application-registration-logo-file-id-column.util.ts` so both paths stay byte-for-byte identical. Class name follows the `Early2_4` / `Early2_5` precedent to avoid colliding with the 2.21 command. The fix lives entirely in 2.23: instances stuck at the failing workspace command retry it every run, and 2.22 -> 2.24 jumps still replay the 2.23 segment. ## Ops note Instances failing right now can be unblocked immediately by running the same `ALTER TABLE` block by hand against their core database (byte-for-byte what the command does). Worth including in the 2.23 patch release note. ## Verification - New fast instance command re-slotted last in the 2.23 fast segment (timestamp `1784823473532` > current max `1784659343818`). - Manual repro path: boot `twentycrm/twenty:v2.21`, seed, stop, run `upgrade` from this branch, assert the column exists and `upgrade:status` reports 0 failed. The default v1.22 baseline does not reproduce it (replays from cursor 0). --- _Generated by [Claude Code](https://claude.ai/code/session_01YAuDR585cx7FyAKoiT32j3)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23215?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Paul Rastoin <paul.rastoin@gmail.com> |
||
|
|
5c7915ba56 |
Skip install apps onboarding step for invitees (#23214)
Users joining an existing workspace via an invitation link were shown the install apps onboarding step after skipping the email-connect step, even though the backend never assigns them that step (and apps they selected were silently not installed). The frontend optimistic transition unconditionally mapped SYNC_EMAIL to APPS_INSTALLATION. It now checks workspaceMembersCount === 1, like the existing PROFILE_CREATION to INVITE_TEAM transition, so invitees go straight to profile creation. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23214?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
5160415f40 |
fix(workflow): create core mirror rows during workflow prefill (#23204)
## Problem Seeded workflows are inserted during workspace activation (and dev seeding) via raw SQL in `prefill-workflows.util.ts`, which bypasses the ORM entirely. The async dual-write listener that mirrors `workflow` / `workflowVersion` into `core.workflow` / `core."workflowVersion"` never fires for these rows, so every newly activated workspace is born with: - no core mirror rows, and - NULL `coreWorkflowId` / `coreWorkflowVersionId` soft-refs. This is permanent, growing drift. The core-consistency check reports every new workspace as 2 unlinked workflows + versions, and once trigger dispatch reads from core these seeded workflows would silently stop working. ## Change Insert the `core.workflow` and `core."workflowVersion"` mirror rows and stamp the soft-refs inside the same prefill transaction. Field mapping mirrors the dual-write / backfill exactly: - `core."workflowVersion".workflowId` = the workspace workflow id (what the trigger-map cache groups by) - `triggers` = jsonb `[trigger]` - `applicationId` = `workspace.workspaceCustomApplicationId` (throws if missing, same contract as the sync path) - `core.workflow.lastPublishedVersionId` = the workspace version id - one ACTIVE core version per workflow (satisfies the partial unique index) Core ids are deterministic v5 (same helper/namespace as the existing prefill ids) so the existing `.orIgnore()` re-run guard stays idempotent — a re-run hits the PK conflict and is skipped instead of inserting duplicate orphan core rows. The large diff is mostly re-indentation: the step/trigger arrays were extracted to consts so the same objects feed both the workspace and core inserts. The step/field UUID literals are unchanged from main. ## Verification - `nx lint:diff-with-main twenty-server`: clean. - Typecheck (isolated `tsc`): no errors in the changed file (`nx typecheck twenty-server` is blocked by a pre-existing `twenty-shared` build error on main, unrelated). - Runtime: pending a `database:reset` + cross-schema parity query confirming zero drift on a freshly seeded workspace. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23204?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
4d09c400a4 |
Remove DATABASE_EVENT_JOBS_CHUNK_SIZE and Promise.all from logic function trigger jobs (#23205)
## What - `LogicFunctionTriggerJob` now processes a single `LogicFunctionTriggerJobData` payload instead of an array processed with `Promise.all`. - Removed `DATABASE_EVENT_JOBS_CHUNK_SIZE` and the `lodash.chunk` usage in `CallDatabaseEventTriggerJobsJob`. - Added `bulkAdd` to `MessageQueueService` and both drivers (BullMQ driver uses native `queue.addBulk`, sync driver processes payloads sequentially). `CallDatabaseEventTriggerJobsJob` uses it to enqueue all payloads in one call. - Updated the other producers (`ServerRouteTriggerService`, `ApplicationInstallService`, `ConnectionProviderOauthFlowService`, `CronTriggerCronJob`) to enqueue a single payload instead of a one-element array, and updated the corresponding specs. ## Why Each logic function execution now gets its own queue job, so a failing execution only retries itself instead of re-running the whole chunk, and job-level retry/metrics apply per execution. --- _Generated by [Claude Code](https://claude.ai/code/session_018VCs2kopnDiZCL41eToxQF)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23205?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
65d399c70d |
feat(page-layout): drag widgets across tabs on record pages (#23023)
## What When editing a record page layout, you can now drag a widget out of one tab and into another. The most common case works: drag from the left column (the pinned first tab, in full mode) into the tab you're currently viewing. Ways to move a widget across tabs: - **Into the visible tab's content** — drop it into another vertical-list tab's list to place it at a precise index. A blue line shows exactly where it will land. - **Onto a tab button** — drop a widget onto another tab's button to move it to that tab; the button highlights while hovered. - **Into an empty tab / the end of a tab** — an end-of-list drop zone (wrapping the add-widget area) accepts the widget, so an empty tab is a valid drop target and widgets can be appended to the end of a populated one. Within-tab reordering keeps working as before, now with the same blue drop-line indicator. ## How The record-page widget list is migrated from `@hello-pangea/dnd` to `@dnd-kit/react` (already used elsewhere in the app, e.g. navigation-menu-item and record-board). A single `DragDropProvider` spans the left column, the tab bar, and the active tab content, which is what makes cross-list drag possible — Pangea scopes each list to its own context, so cross-tab drag wasn't expressible there. - Widgets are dnd-kit sortables grouped by `tabId`; dropping into a different group is a cross-tab move. - The drop line uses `useSortable().isDropTarget` on the targeted widget, plus an end-of-list droppable for append/empty-tab. - Each record-page tab button is a `useDroppable` target for widgets, opt-in per vertical-list tab so canvas/grid tabs keep their native placement. - The drag lifecycle lives in one router hook (`usePageLayoutWidgetDragAndDrop`) that routes to two pure, unit-tested draft utils (`moveWidgetWithinTabInDraft`, `moveWidgetToTabInDraft`). The side-panel "Move to tab" action shares the same `moveWidgetToTabInDraft` util, so drag and menu paths converge on one mutation. - Drop resolution reuses the shared module #23071 landed: `getDestinationIndex` compensates same-tab downward moves for the source-removal shift so the drop line and the landing slot agree, and the shared `preventNativeDragStart` guard stops links/images inside widget content from starting a native URL drag. ## Scope Deliberately staged to the record-page widget list. Not included (follow-ups): - Tab reordering and the field-config editors still use `@hello-pangea/dnd`; finishing the full page-layout removal of Pangea is separate. - Grid/dashboard cross-tab drag (the source there is `react-grid-layout`, which needs a cross-system bridge). - #23071 has merged and this branch sits on it; the remaining convergence is a follow-up: fold `PageLayoutWidgetSortableItem`/`PageLayoutWidgetDropLine` into the shared `DragDropItem*` cells, export a generic drag-event-type helper to delete the 7 copied `Parameters<...>` extractions, replace `useMovePageLayoutWidgetUp/Down` with `moveWidgetWithinTabInDraft`, and migrate the remaining page-layout test suites onto `pageLayoutDraftFixtures`. ## Testing - Unit tests for `moveWidgetToTabInDraft` and `moveWidgetWithinTabInDraft` (incl. the non-vertical destination guard); the three suites now share one fixture module (`page-layout/testing/pageLayoutDraftFixtures`). - The downward off-by-one is covered by the shared `getDestinationIndex` unit tests from #23071. - Full `page-layout` suite green (161 files / 1055 tests), plus typecheck, lint, and format. - The drag interaction itself (drop precision incl. downward same-tab drops, line/highlight, clone feedback) still needs a manual pass in the running app. |
||
|
|
96a2456367 |
feat(workflow): periodic core-consistency check for workflows, versions and triggers (#23103)
Monitoring for the soft-ref migration. The `workflow` /
`workflowVersion` dual-write into core is **best-effort** (async, not
transactional; failures only go to Sentry), so `core.workflow` /
`core.workflowVersion` can silently drift from the workspace source of
truth. This adds a periodic job that detects that drift across **all
three workflow entities** and emits it as metrics.
Supersedes the earlier inline shadow-parity approach that lived on this
branch — that only covered trigger dispatch and added a cache read +
diff to every cron tick and every DB-event batch (too much hot-path
overhead). This is broader and fully off the dispatch path.
## What
A cron (`cron:workflow:core-consistency-check`, every 3 hours, wired
into `cron:register:all`). Per run:
- **Bounded**: `SELECT DISTINCT "workspaceId" FROM core."workflow"` —
only workspaces that actually use workflows (skips the large majority).
- Per such workspace, emit a drift metric per `(entity, driftType)` —
detect-only, plus a triage log:
- **workflow** and **workflowVersion** — `unlinked` / `missingCore` /
`orphanCore` / `fieldMismatch`, via cross-schema `COUNT` aggregates
(core + workspace are the same DB, so indexed joins — no rows pulled
into JS).
- **automated triggers** — the `workflowAutomatedTrigger` table vs the
`workflowAutomatedTriggerMaps` cache: `inTableNotCache` /
`inCacheNotTable` / settings `mismatch`.
- Per-workspace failures are isolated (caught → Sentry) so one bad
workspace does not stop the sweep.
## Why it is efficient
Two central `core.*` queries + a few `COUNT` queries per
*workflow-using* workspace, on a relaxed cadence, off the dispatch path.
Shardable across ticks later if needed.
## Metrics
`workflow-core-consistency/{workflow,version,automated-trigger}/drift`
counters, attribute `driftType`. Dashboards: twentyhq/twenty-infra#800.
## Not in scope
Detect-only — no auto-heal (the existing backfill/rebuild command can
heal). No dispatch or flag changes.
## Test
- The consistency SQL (workflow/version sync counts, orphan counts,
trigger read) validated against a live workspace (it surfaced real drift
there — unlinked versions + an orphan core version). The whole
cron→service→SQL→metric pipeline is proven live: the cron is already
emitting real drift counters on a running server.
- Unit specs for the service (clean → no metric; per-entity drift per
dimension; per-workspace error isolation).
- Command boots and registers via `cron:register:all` (verified).
Typecheck + lint clean.
|
||
|
|
66df0ac47c |
Switch application stop/start commands to Redis-backed global kill switch (#23202)
## Context [#23183](https://github.com/twentyhq/twenty/pull/23183) introduced the right enforcement point: every logic-function execution is rejected centrally before consuming the shared workspace throttle when its application is stopped. However, its server-wide path reads PostgreSQL for every execution attempt. A kill switch is most useful while an application is producing abnormal load, potentially while PostgreSQL is already under pressure. The enforcement mechanism should not add more database traffic in that situation. This state is also operational and temporary. It is used to troubleshoot an application, not as durable application configuration. ## What this PR changes - Uses one global Redis key per application universal identifier: ```text module:applications:kill-switch:{applicationUniversalIdentifier} ``` - Keeps the check in `LogicFunctionExecutorService`, before the workspace execution throttle. - Adds a 60-second process-local cache for both present and absent keys. - Deduplicates concurrent cache refreshes, so an execution burst causes at most one Redis read per application and process. - Fails open when Redis cannot be read and caches that result for the same minute, avoiding a Redis retry storm. - Removes the database columns, upgrade command, workspace-cache recomputation, registration lookup, and stop/start CLI commands introduced by #23183. - Keeps disabled queued executions non-retriable, without emitting one warning for every skipped payload. The switch is operated directly in Redis. For example: ```redis SET module:applications:kill-switch:{applicationUniversalIdentifier} 1 EX 3600 DEL module:applications:kill-switch:{applicationUniversalIdentifier} ``` Any value means stopped; deleting or expiring the key means enabled. ## Why this is a better fit | | #23183 | This PR | |---|---|---| | State | Durable PostgreSQL fields | Ephemeral Redis key | | Server-wide hot path | PostgreSQL lookup per execution | At most one Redis lookup per app/process/minute | | Scope | Workspace and application registration | Application universal identifier across all workspaces | | Operational cleanup | Explicit start command | `DEL`, eviction, restart, or operator-selected TTL | | Database dependency during an incident | Required | None | The trade-off is deliberate: a Redis change can take up to 60 seconds to reach every process, and the switch is lost when the cache key disappears. That is acceptable for a temporary troubleshooting control and keeps the normal execution path inexpensive. Existing in-flight functions are not interrupted. New direct or queued executions are rejected when they reach the executor. |
||
|
|
8368dd41c4 |
Remove legacy FindAllViews response cache flush from migration runner (#23190)
The pattern-flush (a full Redis keyspace SCAN) was kept for exactly one release after the metadata GraphQL caches were re-keyed on flat-map hashes (#23164), as the only invalidation signal for old pods' version-keyed FindAllViews entries during rolling deploys. With all pods on hash-keyed caches, dependency-hash rotation in the cache key covers both view and metadata changes, so the flush is redundant. flushGraphQLOperation has no remaining callers, so it is deleted from WorkspaceCacheStorageService as well. incrementMetadataVersion stays: it still feeds the X-Schema-Version check. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23190?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
15f571837e |
fix: bump js-yaml pins 4.2.0 -> 4.3.0 (Dependabot) (#23178)
## Summary Bumps all nine scoped **js-yaml resolutions 4.2.0 -> 4.3.0** and lifts the caret copy, clearing Dependabot alert [1768](https://github.com/twentyhq/twenty/security/dependabot/1768): **GHSA-52cp-r559-cp3m / CVE-2026-59869** (high) - YAML merge-key chains force quadratic CPU consumption, vulnerable `>= 4.0.0, < 4.3.0`, fixed **4.3.0**. Follow-up to the merge-key DoS fixed in 4.2.0 (GHSA-h67p-54hq-rp68). ## Why the pins move (not drop) Checked upstream first: all seven 4.1.1 exact-pinners are unchanged at latest (`@mintlify/cli@4.0.1331`, `@mintlify/common@1.0.1037`, `@mintlify/prebuild@1.0.1185`, `@mintlify/previewing@4.0.1254`, `@mintlify/scraping@4.0.902`, `@mintlify/validation@0.1.795`, `@verdaccio/config@8.1.2` - every one still pins `js-yaml 4.1.1` exact). front-matter and @istanbuljs/load-nyc-config remain EOL on `^3.13.1`. So no parent upgrade carries 4.3.0; the existing scoped pins just move up, plus a recursive `yarn up` for the cosmiconfig caret consumers. The `//resolutions` doc entry is updated with the new advisory and drop condition (`>=4.3.0`). ## Verification - Single `js-yaml 4.3.0` entry remains in the lockfile (no 4.2.0, no 3.x). - `yarn install --immutable` passes. - front-matter patch intact (`loader = parser.load`); docs front-matter parses cleanly on 4.3.0. - `mintlify validate` reports only pre-existing ChartIcon MDX import warnings from #23091 (content, unrelated - zero `.mdx` files in this diff). - 4.3.0 published 2026-06-26, clears the 3-day age gate. |
||
|
|
5c825e8712 |
fix(ai-chat): write the stream heartbeat before the DB claim (#23198)
## Problem Answering an `ask_questions` (select) prompt sometimes killed the turn with "Failed to get response. The response was interrupted before it could finish." The answer was swallowed and Retry rewound the whole turn. It was intermittent, worse on long threads and when coming back from another tab. Reported in [discord quality issue](https://discord.com/channels/1130383047699738754/1526875783170097172). Confirmed in prod: ~28 `ai_chat_turn_failed_total{failure_phase="interrupted"}` over the last 7 days (the only failure phase firing), plus matching `the thread no longer holds this claim` worker logs around the report time. ## Root cause A stream is tracked by two records: the claim (`activeStreamId` in Postgres) and the heartbeat (a Redis key refreshed while the worker runs). `reapDeadStream` treats "claim set but no heartbeat" as a crashed worker and kills the turn. On the answer path the ordering left a window where that was falsely true: 1. `resolvePendingQuestion` writes `activeStreamId` to Postgres (claim set) 2. `enqueueResumeStream` reloads the thread and runs `loadMessagesFromDB` (reads every message and part, signs a URL per file, hundreds of ms on long threads) 3. only then `markClaimed` writes the heartbeat Between 1 and 3 the thread looks dead to the reaper. Worse, `question-answered` was published inside that window, so the client refetched, and the refetch's `chatStreamCatchupChunks` query runs the reaper, racing the server into its own setup window. The keepalive reap tick could land there too. ## Fix Enforce one invariant everywhere: the heartbeat exists before any DB row carries the `activeStreamId`, so "claim without heartbeat" can only ever mean a genuinely dead worker. - New `answerPendingQuestionAndResumeStream` owns the answer flow: `markClaimed` first, then the DB claim, then enqueue, then publish `question-answered` (moved after the enqueue so client refetches can't race the setup, and so we don't tell the client "answered" when the enqueue failed and rolled back). - Both failure paths clean up: clear the heartbeat if resolving fails; restore the pending question and clear the heartbeat if enqueueing fails. - `tryClaimStream` (send / retry / queue-flush) reordered the same way: heartbeat before the claim, cleared if the claim is lost. - `releaseStreamClaim` now also clears the heartbeat so failed claims leave no orphan key. No grace period or schema change needed: the ordering closes the race structurally. The Retry-rewinds-the-turn behavior is unrelated and left as a separate follow-up. ## Testing - New `agent-chat-streaming.service.answer.spec.ts`: heartbeat marked before the claim, publish only after enqueue, both failure paths restore state and clear the key. - Extended `agent-chat-streaming.service.claim.spec.ts`: heartbeat-before-claim ordering and key cleanup on lost claim / failed enqueue. - Full ai-chat suite green (74 tests), lint and typecheck clean. After deploy, `sum(increase(ai_chat_turn_failed_total{failure_phase="interrupted"}[1d]))` trending to zero confirms the fix. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23198?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
9d8ce7c325 |
v1.3.2 — Modularize partners app into vertical-slice modules/ (#23168)
**Version:** `twenty-partners@1.3.2` (patch — internal refactor, no
visible behavior change)
## What & why
Reorganizes the `twenty-partners` SDK app from a flat, type-first layout
(`src/{objects,fields,views,logic-functions,front-components,…}`) into a
**vertical-slice** layout under `src/modules/<domain>/<feature>/`. Files
that
change together now live together; each SDK entrypoint is a thin
discoverable
shim over a service + graphql-ops + mapper/connector layer.
This is a pure structural refactor — **no object, field, view, enum,
logic
function, trigger, role, or application variable changed.**
## Final layout
```
src/modules/
shared/ http · services · graphql · utils · front-components · navigation-menu-items (cross-domain nav folders)
opportunity/ fields·view-fields·views·navigation-menu-items·page-layouts·constants + intake/ + matching/
partner/ objects·fields·constants·utils + directory/ · self-service/ · marketplace/ · application-intake/ (Discord connector/)
application/ objects·fields·views·navigation-menu-items·page-layouts + services · graphql
```
Every `defineLogicFunction` entrypoint is now a thin
`*.logic-function.ts`
(all < 40 lines) at its domain/feature root, delegating to a
`*.service.ts`;
graphql operations live in `graphql/{queries,mutations}/`, pure
transforms in
`mappers/`, outbound APIs (the Discord webhook) in `connector/`, pure
helpers
in `utils/`.
## Safety — the load-bearing invariant
The server diffs app primitives by `universalIdentifier`, so a
changed/dropped
UUID would drop-and-recreate the object on prod (data loss). This branch
holds
that line:
- **887 `universalIdentifier`s byte-identical** to the branch base
(every
relocation is a `git mv`; every extracted entrypoint keeps its original
UUID/name/trigger verbatim). Re-verified byte-identical across the
rebase.
- `yarn twenty dev --once` against a live workspace = **"No changes.
Twenty
metadata matches your manifest."**, confirmed idempotent on a second run
—
the whole refactor is a metadata no-op (zero create/delete/identity
change).
- Every extracted graphql op was verified **byte-identical** to its
original
(args, `first:` caps, pagination, selection sets), and the
partner-application
Discord embed's deliberate PII omission (no email / hourly rate) is
preserved.
## Rebased onto latest `main`
This branch is rebased onto `main` (`d20e5378fd`) and now carries main's
`twenty-sdk` / `twenty-client-sdk` **2.23.0-alpha.2** bump.
Note for reviewers: main had independently bumped this package to
`1.3.1`, so
the original `1.3.0 → 1.3.1` commit here was redundant and git dropped
it during
the rebase (`patch contents already upstream`) — with **no textual
conflict**,
since both sides wrote the same version string. The bump is therefore
now
**`1.3.2`**. The rebase touched only `package.json` and `yarn.lock`;
**every line
of refactored source is byte-identical** to the pre-rebase tree.
## Verification
All run on the rebased tree, against SDK `2.23.0-alpha.2` and a live
Twenty
server `v2.23.2`:
| Check | Result |
|---|---|
| `universalIdentifier` set | 887, byte-identical |
| `yarn twenty dev --once` | "No changes" (idempotent on re-run) |
| Typecheck | pass |
| `yarn lint` | 0 warnings, 0 errors (287 files) |
| `yarn test:unit` | 158/158 (23 files) |
| `yarn test:integration` | 45/45 (13 files) |
## Also in this PR
- **Architecture convention doc** — `AGENTS.md` (+ a one-line
`CLAUDE.md` pointer)
at the package root documents the vertical-slice conventions this
refactor
establishes: the layout, the dependency rule (`logic-function → service
→
graphql/connector`), file naming, connector = outbound-only (inbound
webhooks
are logic-functions), and the UUID invariant. It ships here so the doc
and the
structure it describes land together.
- **`modules/shared/`** dedup: the secret-guarded intake envelope, the
find-or-create-company/person helpers + their graphql ops, `collectAll`
pagination, `http-url`/`strip-markdown`/`is-non-empty-string` utils.
- **Vitest configs collapsed** into one `vitest.config.ts` with `unit` +
`integration` projects (`yarn test:unit` / `yarn test:integration`).
- Cross-domain nav folders (`pipeline-folder`,
`partner-workspace-folder`)
hoisted to `modules/shared/navigation-menu-items/`.
## Deferred (non-blocking, tracked follow-ups)
- Add direct unit tests for the shared `collectAll` / `isNonEmptyString`
utils
(currently covered indirectly).
- Move `submit-client-brief`'s zod schema out of its mapper file into
its own
schema file (mirroring the partner side).
- Route `stamp-partner-user-on-child` through the shared self-service
mutation ops.
- `find-partner-by-member.ts` is duplicated identically in the
`application` and
`self-service` domains; a candidate to hoist into `modules/shared/`.
|
||
|
|
d20e5378fd |
i18n - translations (#23200)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
f5a9adcb76 |
Add post-onboarding AI chat setup behind a feature flag (#23120)
https://github.com/user-attachments/assets/fec7076f-4e46-4c39-84d7-68e4340244ac After finishing onboarding, users now land in a full-screen AI chat that helps them set up their workspace, instead of going straight to their default view. The welcome overlay's title flies into the chat's first message so the handoff reads as one continuous motion: the slide plays alone, the title swaps in place pixel-exactly (a regular-weight clone of the target line is crossfaded in mid-flight to morph the font weight), then the rest of the text fades in. All of it sits behind `IS_ONBOARDING_AI_CHAT_ENABLED` (default off, not registered as a public flag). With the flag off, onboarding behaves exactly as it does today — the welcome overlay still plays and the user lands on their home view. Layout follows the Figma: the nav drawer stays visible and the chat renders in a panel-styled container with an "Onboarding" header, matching the expanded side panel. Also fixes two pre-existing bugs the feature surfaced: - On billing instances the completion redirect raced the lazy `PaymentSuccess` page, which silently skipped the welcome animation on the no-card trial path. The redirect now defers while a checkout is pending, and `PaymentSuccess` always confirms through `useLoadCurrentUser` so freshly served feature flags are respected. - `useDefaultHomePagePath` could conclude its `/settings/profile` empty-workspace fallback from a transiently empty metadata store and strand the user there; it now waits for both object metadata and navigation menu items before deciding. Reviewer notes: - `AgentChatRuntimeEffects` no longer keys off side-panel state, so `modules/ai` stops importing `modules/side-panel`. The two visibility-scoped effects moved into `AiChatTab`. - `/workspace-setup` is deliberately URL-addressable rather than onboarding-only: the collapse control in the header is a general expand/collapse toggle (paired with a new expand button in the side panel top bar), and gating the route would break refresh and browser-back. It is still authenticated-only. - The design's second, LLM-authored paragraph is not implemented — starting an assistant turn with no user message needs server-side work. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23120?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
e15b9efc2d |
Deprecate workspace metadataVersion and stop consuming it in the frontend (#23189)
## Context Follow-up to #23164. Now that the metadata GraphQL response cache and the workspace SDL cache are keyed on flat-map hashes, `workspace.metadataVersion` no longer drives any cache invalidation. This PR is the next stage of retiring it: the frontend stops consuming the field entirely, and the public GraphQL field is marked deprecated so external API consumers get a migration signal. ## What changed **Frontend stops consuming `metadataVersion`:** - `userQueryFragment.ts` no longer selects the field. - `currentWorkspaceState.ts` drops it from the workspace `Pick`. - `apollo.factory.ts` no longer attaches the `X-Schema-Version` request header. Dropping the header retires the "your workspace has been updated, please refresh the page" error rewrite on the server (it only fired when the header was present, and only on requests that had already failed validation). Metadata staleness detection is unaffected: the frontend has been running on collection hashes plus SSE since the minimal-metadata work, so that path stays intact. Stale clients now surface a raw validation error instead of the friendly message, which we consider an acceptable trade for deleting the mechanism. **Server marks the field deprecated:** - `workspace.entity.ts`: `@Field({ deprecationReason: 'No longer used for metadata cache invalidation, will be removed' })`. **Regenerated (CI-enforced surfaces):** `twenty-front/src/generated-metadata`, and `twenty-client-sdk`'s generated schema, which now carries `@deprecated(reason: ...)`. The `admin` codegen config produced no changes. `packages/twenty-sdk/generated` is intentionally untouched: no in-repo command produces it, CI does not drift-check it, and its committed snapshot lags the live schema, so regenerating it here would pull unrelated schema drift into this PR; it will pick up the directive on its next routine refresh. ## Deployment notes - No ordering constraint with #23164: removing a field selection and a request header is backward compatible against any server, and old frontend bundles keep working during the rollout because the field still exists and the server-side header check is still in place. Same release is fine. - The follow-up server cleanup (removing the `X-Schema-Version` check in `use-graphql-error-handler.hook.ts`, the per-request `metadataVersion` reads and seed in `middleware.service.ts`/`jwt-auth.guard.ts`, and the REST heal block) must wait until the release containing this PR has shipped, since a deployed frontend still selecting the field would break `GetCurrentUser` if the field were removed first. After that cleanup, the only remaining `metadataVersion` consumers are the five pinned upgrade commands (2.8 through 2.20), which hold the column and `WorkspaceMetadataVersionService` until that upgrade window closes; the physical column drop then follows the two-phase pattern used for `gridPosition`. ## Validation - Server and frontend typecheck, lint, and format pass; the apollo factory test suite passes unchanged (it fixtures the field but never asserted the header). - Live introspection against a server running this branch returns `isDeprecated: true` with the reason on `Workspace.metadataVersion`. - CI's pending-codegen check covers the regenerated surfaces (`data`/`metadata`/`admin` configs and `twenty-client-sdk:generate-metadata-client`). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23189?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
d1c70ab0bf |
Fix dashboard record table widget aggregate persistence (#23008)
## Summary - Update dashboard record-table widget aggregate changes to write into the widget draft while page layout edit mode is active. - Include `aggregateOperation` when saving record-table widget view fields through `upsertViewWidget`. - Persist aggregate operations server-side for widget view-field create, update, and clear flows. - Add frontend utility tests and backend integration coverage for widget aggregate create/update/clear behavior. Fixes #22934. ## Why Dashboard record-table widgets use their own draft view state while a page layout is being edited. The aggregate footer path was resolving fields through the normal current-view flow and then trying to persist immediately, which can miss widget draft fields and fail before the save flow runs. This change keeps aggregate edits in the widget draft during page layout editing, then saves the aggregate operation with the rest of the widget view configuration. ## Validation - `npx nx lint twenty-front` - `npx nx typecheck twenty-front` - `npx nx test twenty-front --configuration=ci` - `npx nx build twenty-front` - `npx nx build twenty-server` - `npx nx lint twenty-server --configuration=ci` - `npx nx typecheck twenty-server` - `npx nx test twenty-server --configuration=ci` - `npx nx jest --config ./jest-integration.config.ts --logHeapUsage --runTestsByPath test/integration/metadata/suites/view/upsert-view-widget.integration-spec.ts` - `git diff --check` Disclosure: I used AI-assisted coding tools while preparing this PR. I reviewed the changes myself, tested them, and take responsibility for the implementation and any follow-up revisions needed. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23008?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
32041ce2e0 |
fix(website): restore condensed marketplace list card (#23161)
## Summary The condensed marketplace list-card design (sans-serif partner name, description excerpt, a single scope line, and a "View profile →" text CTA) was the intended glowup card, shipped by #22471/#22402. During the #23016 resync, the `PartnerCard.tsx` merge conflict was resolved in favor of main's rich card, so the condensed design never actually landed on `main`. This PR restores the condensed design, ported onto main's current data layer and helpers — none of #23016's profile/case-study/matching work is touched. ## What changed - `PartnerCard.tsx` — rewritten to render the condensed layout (sans-serif `PartnerName`, `CardIntro` description excerpt, `CardFoot` with a single `ScopeLine` and a mono `CardCta` "View profile →" text link, no chip rows, no money row, no LinkedIn icon). Reuses main's data layer and helpers directly: `richTextExcerpt`, `resolvePartnerScopeCards`, `PartnerAvatar`, `titleCaseFallback`, and the shared `CardFrame` shell (same entrance-animation/hover idiom already used by `MarketplaceMatchCard`). No new helper files were needed — everything the condensed design requires already exists on `main`. - `PartnerChipRow.tsx` — deleted (zero remaining importers after the port; grep confirmed only self-reference). - `PartnerMoneyRow.tsx` — deleted (zero remaining importers after the port; grep confirmed only self-reference). Kept untouched: `fetch-live-marketplace-partners.ts`, `marketplace-partner.ts`, `get-marketplace-partners.ts`, `marketplace-partners-source.ts`, all `PartnerProfile*`/case-study/matching files, `MarketplaceGrid.tsx` (already has the `repeat(N, minmax(0, 1fr))` grid columns on main — no change needed), and the shared label helpers `PARTNER_SCOPE_LABELS`/`SERVED_GEO_LABELS`/`SPOKEN_LANGUAGE_LABELS` (still used by `FilterBar.tsx`/`PartnerReachFacts.tsx`). Website-only change — no partners-app version bump, no `.po` catalog changes. ## Test plan - [x] `npx nx typecheck twenty-website` — clean - [x] `npx oxlint -c .oxlintrc.json .` (twenty-website) — 0 warnings, 0 errors - [x] `npx oxfmt --check .` (twenty-website) — clean - [x] `npx jest partners-marketplace` — 12 suites / 54 tests passing - [x] Verified locally against the running dev server (`/partners/list`): 16 partner cards render with the condensed layout (sans-serif name, excerpt, single scope line, "View profile →"), no chip rows/money rows/LinkedIn icons, real partner data renders correctly (e.g. 01GROWTH, Inc) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23161?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
e2ea82170c |
i18n - translations (#23191)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23191?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
a3f4acadb1 |
Add workspace and server level stop commands for applications (#23183)
## Context When an installed application misbehaves (e.g. a logic function loop DDoSing the server or the database), we currently have no targeted way to shut it down in production: the only kill switch is `LOGIC_FUNCTION_TYPE=DISABLED`, which disables logic functions for the whole instance. This PR adds an emergency stop mechanism at two levels: - **Workspace level**: stop one installed application in its workspace. - **Server level**: stop every application installed from an `applicationRegistration`, across all workspaces. ## How it works **New nullable `stoppedAt` columns** on `core.application` and `core.applicationRegistration` (fast instance command `2.24.0`, with `up`/`down` and `@WasIntroducedInUpgrade` decorators on the entities). **Enforcement in a single choke point**: `LogicFunctionExecutorService.execute()` is the funnel behind every execution path (public route triggers, server route triggers, cron triggers, database event triggers, workflow actions, agent tool calls, manual GraphQL execution, install hooks). A new `assertApplicationNotStopped` guard runs right after the flat entities are resolved and throws `LOGIC_FUNCTION_DISABLED` (already mapped to a 403 on route triggers and handled by the GraphQL exception handler) when: - `flatApplication.stoppedAt` is set (workspace-level stop, read from the cached flat application maps: zero extra runtime cost), or - the linked registration is stopped (one indexed PK lookup, same pattern as the existing per-execution server-variable query). **Propagation**: the workspace-level stop invalidates and recomputes `flatApplicationMaps` for the workspace, so all server instances pick the flag up within the local cache TTL (100ms). The registration-level flag is read live, so it is effective immediately. ## Ops commands ```bash # Workspace level yarn command:prod application:stop -a <application-id> yarn command:prod application:start -a <application-id> # Server level (all applications of the registration, all workspaces) yarn command:prod application-registration:stop -r <application-registration-id> yarn command:prod application-registration:start -r <application-registration-id> ``` Each command logs what was stopped/started and, for registrations, how many installed applications are affected. ## Notes - Stopped executions fail fast at the guard, so queued trigger jobs (cron/db-event) burn a negligible amount of work while stopped. - The two flags are independent: lifting a registration-level stop does not clear workspace-level stops that were set individually, and vice versa. - Unit tests added for `ApplicationStopService`. --- _Generated by [Claude Code](https://claude.ai/code/session_01CjEnKUACn89aSgK1wEMH2d)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23183?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
a65da48591 |
feat(server): per-worker queue filtering via env vars (#23181)
## Context Follow-up to #23134. Goal: stop a heavy/long-running queue from saturating every worker pod and blocking the whole job pipeline, by letting each BullMQ worker decide **which queues it consumes** from env vars. > Note: an earlier revision of this PR also added a dedicated `application-queue` (concurrency 1). Per review, that was dropped — application install/upgrade/backfill jobs stay on `workspaceQueue`. This PR now contains **only** the worker queue-filtering mechanism. ## What changed - The queue-worker explorer (`MessageQueueExplorer`, which only runs in the `queue-worker` process) now reads two env vars before creating workers: - `WORKER_ENABLED_QUEUES` — comma-separated allowlist of queues this worker processes (empty = all). - `WORKER_EXCLUDED_QUEUES` — comma-separated denylist, applied after the allowlist. - Workers are only created for queues that pass the filter; filtered-out queues are logged and skipped. Unknown queue names are logged as warnings. - Both vars are read directly from `process.env` (not the DB-backed config-variable system), since they're worker-bootstrap settings. - Pure decision logic + env parsing extracted to `shouldCreateWorkerForQueue` / `parseQueueListFromEnv` utils with unit tests. Queue **clients** are still registered in every process, so jobs can be enqueued from anywhere — only the **consumer** side is gated. ## Usage Isolate `workspace-queue` (where the application jobs run) onto dedicated pods: - General worker pods: `WORKER_EXCLUDED_QUEUES=workspace-queue` - Dedicated worker pods: `WORKER_ENABLED_QUEUES=workspace-queue` ## Tests - `should-create-worker-for-queue.util.spec.ts`: allowlist / denylist / precedence + env parsing. - `typecheck` + `oxlint --type-aware` + `oxfmt --check` clean on the diff. ## Companion - twentyhq/twenty-infra#805 wires `WORKER_ENABLED_QUEUES` / `WORKER_EXCLUDED_QUEUES` to the worker pods. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_014XeN6wVSbMWnFu8jeLaSXk --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3ae09cd581 |
i18n - translations (#23187)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
987995636f |
Sync metadata store after view child entity mutations server response (#23150)
## Summary - View persist hooks (`usePerformView(Sort|Field|Filter|Group|FieldGroup|FilterGroup|)APIPersist`) now write successful mutation results back to the metadata store (`addToDraft` / `updateInDraft` / `removeFromDraft` + `applyChanges`), following the existing pattern from `usePerformViewAPIUpdate`. - Previously the store only updated via SSE, so until that landed, save flows diffed against stale view data and re-sent creates with the same id — failing server-side with a duplicate key error. Fixes TWENTY-SERVER-HQM |
||
|
|
f52643d170 |
i18n - docs translations (#23186)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
0108a34765 |
Rekey metadata caches when flat map hashes change (#23164)
## Context
The `/metadata` GraphQL response cache (`ObjectMetadataItems`,
`FindAllViews`) and the workspace SDL cache were keyed on
`workspace.metadataVersion`, an integer bumped on every object/field
migration. That mechanism is legacy (the migration runner literally
calls it `getLegacyCacheInvalidationPromises`): the data plane already
moved to `WorkspaceCacheService`, which versions each flat entity map
with its own hash minted on invalidation.
Version keying had two concrete costs: the version bump was the only
proactive invalidation for `ObjectMetadataItems`, and keeping
`FindAllViews` fresh required `flushGraphQLOperation`, a full Redis
keyspace SCAN on every relevant migration. It is also the main blocker
for deprecating `metadataVersion` entirely.
This PR re-keys both caches on the flat-map hashes instead.
## What changed
**Response cache** (`use-cached-metadata.ts`): the key is now
`{operation}:{workspaceId}:{combinedDependencyHash}[:{userWorkspaceId}]:{locale}:{queryHash}`.
Each cached operation declares which flat maps its resolvers read
(`metadata-graphql-operations-to-cache.constant.ts`) plus a scope:
`ObjectMetadataItems` stays workspace-shared, `FindAllViews` is per-user
because unlisted-view visibility depends on the caller. When any
declared map changes, its hash rotates and the key rotates with it; no
flush needed. The key is resolved once per request and reused in
`onResponse`, so a rotation mid-request can never cache a response under
a fresher key than the data it was built from. If hash resolution fails,
the request is served uncached (Sentry-captured).
This also fixes three pre-existing key soundness gaps: `FindAllViews`
ignored locale although view names are translated server-side, the query
hash ignored GraphQL variables (`$viewTypes`), and mid-request version
rotation could re-key between request and response.
**SDL cache** (`workspace-graphql-schema-sdl.service.ts`): keyed on the
combined hash of the four maps the schema is generated from, taken from
the same `getOrRecomputeWithHashes` call that returns the data, so key
and content cannot skew. The `metadataVersion` read/seed block there is
gone; the Redis seed moved to `middleware.service.ts` so the
`X-Schema-Version` "refresh the page" check keeps working after the
Redis key's TTL expires.
**`WorkspaceCacheService`**: the internal pipeline now threads `{data,
hashes}` through every stage (local hit, hash validation, Redis fetch,
recompute) and the memoizer stores the pair, so returned hashes are
always consistent with returned data. New public
`getOrRecomputeWithHashes` and `getOrRecomputeCombinedHash`
(hashes-first: one MGET of the small `:hash` keys, full pipeline only
for missing ones, so cold pods never pull map payloads just to build a
key).
**Atomic pair writes** (`cache-storage.service.ts`): `mset` on the Redis
driver now delegates to the store's own `mset` (a MULTI of `SET ... PX`,
or native `MSET`), grouped by TTL. Previously it was a `Promise.all` of
independent SETs, so two concurrent recomputes could interleave and
leave one recompute's `:data` next to the other's `:hash`; with
hash-keyed caches that torn pair could persist a stale response under a
live key. `CoreEntityCacheService` writes through the same method and is
fixed for free.
**Cleanup**: `flush()` lost its `metadataVersion` parameter (always
pattern-flush per key on workspace deletion),
`METADATA_VERSIONED_WORKSPACE_CACHE_KEY` became
`HASH_KEYED_WORKSPACE_CACHE_KEYS` with the `MetadataVersion` key
relocated to `WORKSPACE_CACHE_KEYS` and the dead `ORMEntitySchemas`
entry removed.
## Deliberately unchanged
- `incrementMetadataVersion` and all its callers stay: the version still
feeds the `X-Schema-Version` check and the pinned upgrade commands.
Deprecating the column is a later stage.
- The runner's `FindAllViews` pattern-flush is kept for exactly one
release: view-only migrations never bump `metadataVersion`, so old pods
in a rolling deploy have no other invalidation signal for their
version-keyed entries. It gets deleted next release, which removes the
SCAN entirely.
- Old-shape cache entries are not migrated; they expire via the 7-day
TTL.
## Known limitations (follow-ups, not regressions)
- The plugin reads dependency hashes Redis-fresh while resolvers can
serve up to 10s-old memoized data, so a request landing right after a
migration can cache a pre-rotation response under the new key. Same
shape existed under `metadataVersion`; closing it needs request-scoped
snapshot plumbing.
- Concurrent recomputes are last-writer-wins (lost update). Fencing with
a conditional write is a follow-up.
## Validation
- Unit: response-cache plugin behavior (scope, key stash, serve-uncached
on failure, prototype-name guard), atomic `mset` batching, existing
`WorkspaceCacheService` spec passing unchanged.
- Integration: a new drift-guard spec runs the real
`ObjectMetadataItems`/`FindAllViews` operations with full frontend
selection sets against the in-process app, spies on
`WorkspaceCacheService`, and fails if resolvers read a flat map missing
from the declared dependency lists, so the constant cannot silently
drift.
- Manual against a live server: creating a field rotates the field-map
hash and the very next `ObjectMetadataItems` response contains it (hash
rotation is now its only invalidation path); warm hits are ~5ms; SDL
entries appear under hash-shaped keys via introspection.
## Suggested reading order
1. `workspace-cache.service.ts`, `workspace-cache-key.type.ts`,
`combine-cache-hashes.util.ts` (the `{data, hashes}` pipeline)
2. `use-cached-metadata.ts`,
`metadata-graphql-operations-to-cache.constant.ts`,
`metadata.module-factory.ts` (response cache)
3. `workspace-graphql-schema-sdl.service.ts`,
`workspace-cache-storage.service.ts` (SDL cache and renames)
4. `middleware.service.ts` (metadata version seed relocation)
5. `cache-storage.service.ts` (atomic writes)
6. Tests
|
||
|
|
f67e9c6b05 |
fix(ai-chat): disable Responses API storage for Azure models to stop "Item with id ... not found" stream failures (#23182)
## Problem AI chat and workflow-agent turns on Azure-routed reasoning models (e.g. `azure-foundry-us/gpt-5.5`) intermittently die mid-answer with: > Failed to get response — `400 Item with id 'rs_...' not found` ## Root cause The OpenAI Responses API is stateful: each output item gets a server-side id (`rs_...` reasoning, `fc_...` function call). With `store: true` (the API default), the Vercel AI SDK replays prior assistant reasoning as `item_reference` entries that the provider must resolve from its own storage. For reasoning models the chain-of-thought is never returned in plaintext — it lives only as a stored referenced item, or as `encrypted_content` requested via `include: ['reasoning.encrypted_content']` (which the SDK only auto-adds when `store: false`). PR #20888 (June 22) switched to the safe `store: false` mode, but only for `AI_SDK_OPENAI`. `AI_SDK_AZURE` fell through to `default` and got no options, so every Azure request ran in stored-reference mode. Azure's item storage resolves those references unreliably (a server-side race — the July 21 failure could not find the exact `rs_...` id Azure itself had streamed seconds earlier in the same turn), and the failure is fatal to the stream. Retries replay the same persisted references, so they can fail again. ## Fix Add an `AI_SDK_AZURE` case in `getCallLevelProviderOptions` that passes `azure: { store: false }`. Both AI chat and workflow agents go through this helper, so both paths are covered. The provider-options key for `@ai-sdk/azure` is `azure` (its responses model is registered as `azure.responses`); I verified the `@ai-sdk/openai@3.0.54` copy nested inside `@ai-sdk/azure` has the same guards as the root `3.0.71`. With `store: false` the SDK: - auto-requests `include: ['reasoning.encrypted_content']` for reasoning models (the `gpt-5.5` deployment name passes reasoning detection), - replays reasoning as self-contained encrypted items instead of server-side references, - drops any stale unencrypted reasoning parts instead of referencing them. ### Transition safety Existing threads whose persisted reasoning has `reasoningEncryptedContent: null` simply have those parts skipped on replay. I checked prod logs for the related pairing-validation error ("was provided without its required...") and found zero occurrences since OpenAI-direct made this same switch a month ago, so the transition is safe. ## Testing - Added two Azure cases to `provider-options.util.spec.ts` — all 9 pass. - oxlint (type-aware) and oxfmt clean on the changed files. ## Post-deploy validation Inverse of the evidence: new Azure reasoning parts should persist with non-null `reasoningEncryptedContent`, and the Loki query below should go quiet. fixes : https://discord.com/channels/1130383047699738754/1526873169124659230 |
||
|
|
c54ad3c0b9 |
feat(ai-instrumentation): fix AI histogram buckets & add tool-call duration instrumentation (#23173)
## What & why
Two related observability fixes for AI chat / agent / MCP metrics.
### 1. Widen histogram buckets (`widden-ai-histo`)
Latency percentiles for AI chat pinned at exactly **10s** on the
dashboard — not because anything times out, but because the histogram
buckets top out at 10,000.
`recordHistogram` created histograms without explicit bucket boundaries,
so the OTel SDK applied its defaults: `[0, 5, 10, 25, 50, 75, 100, 250,
500, 750, 1000, 2500, 5000, 7500, 10000]`. These were designed for small
generic values; interpreted as **ms**, the last finite bucket is exactly
10s. Every observation above 10s falls into the `+Inf` overflow bucket,
and quantile estimation clamps to the highest finite bound — so any
percentile that lands in the overflow draws a flat line at 10s.
Reality (gpt-5.5, last 30 days): 64% of turns exceed 10s (so even p50
pins at 10s), mean turn latency ~38s, max ~29min. The same 10k cap
affects the token-unit `tool-output-tokens` histograms.
**Changes:**
- `recordHistogram` now accepts an optional `bucketBoundaries`, applied
per-instrument via `advice.explicitBucketBoundaries` (supported in
`@opentelemetry/api@1.9.1`). Colocated with the metric, no
instrument-setup changes.
- Added bucket-boundary constants:
- `AI_LATENCY_MS_BUCKET_BOUNDARIES` — `[250, 500, 1000, 2500, 5000,
10000, 20000, 30000, 60000, 120000, 300000, 600000]` (250ms → 10min)
- `TOOL_OUTPUT_TOKENS_BUCKET_BOUNDARIES` — `[100, 250, 500, 1000, 2500,
5000, 10000, 25000, 50000, 100000, 250000, 500000]` (100 → 500k tokens)
- Wired boundaries into `ai-chat/turn-latency-ms`,
`ai-chat/step-latency-ms`, `ai-chat/ttft-ms` (latency) and
`ai-chat/tool-output-tokens`, `workflow-agent/tool-output-tokens`,
`mcp/tool-output-tokens` (tokens).
### 2. Instrument tool-call duration (`add-tool-duration-monitoring`)
Previously we tracked tool success/failure counts and output tokens, but
not how long each tool call took. Added a `*/tool-execution-duration-ms`
histogram for each execution path.
**Changes:**
- New metric keys: `ai-chat/tool-execution-duration-ms`,
`workflow-agent/tool-execution-duration-ms`,
`mcp/tool-execution-duration-ms`.
- New constant `TOOL_EXECUTION_DURATION_MS_BUCKET_BOUNDARIES` — `[25,
50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000, 120000]` (25ms
→ 2min).
- **AI chat / agent node**: use the AI SDK's
`experimental_onToolCallFinish` callback (`ai@6.0.97`), which reports an
exact `durationMs` measured around the tool's `execute()`, plus
`toolCall.toolName` and `success`. Attributes `{ model, tool }`.
- **MCP**: measured directly around `tool.execute` with
`performance.now()`, recorded on both success and failure paths.
Attributes `{ tool }`.
## Notes
- Backward-compatible: metrics without `bucketBoundaries` (e.g.
`job/latency-ms`, `sdk-client-generation/duration-ms`) keep OTel
defaults.
- **Historical data stays clamped** — only writes after deploy use the
new buckets, so percentile panels will show a step change at deploy
time. For truth on existing data, use a mean panel (Sum/Count is exact)
or Sentry trace span durations.
- Provider-executed tools (e.g. native web search) run inside the
provider, so they don't emit a local duration — same limitation as the
existing tool counters.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23173?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
|
||
|
|
0326e32b9b |
i18n - translations (#23184)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23184?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
04d1c2035c |
feat(connections): run a logic function on connection provider connect (#23167)
## What Adds an optional `onConnectLogicFunctionUniversalIdentifier` field to the connection provider manifest. When set, the referenced logic function is dispatched right after an OAuth connection is successfully established for that provider. This gives apps a first-class "on connect" hook — e.g. the Slack app can resolve the workspace's `team_id` via `auth.test` and claim the `team_id -> workspaceId` mapping in the SERVER key-value store immediately on connect, instead of racing against later events. Follow-up to the app key-value store PR (#23089). ## How - **twenty-shared**: add `onConnectLogicFunctionUniversalIdentifier` to `ConnectionProviderManifest`. - **twenty-sdk**: expose the field in `defineConnectionProvider` and validate it is a UUID `universalIdentifier`. - **twenty-server**: - add a nullable `onConnectLogicFunctionUniversalIdentifier` column to `ConnectionProviderEntity` (+ fast instance command / migration). - map the field through the <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23167?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
50ec77daf8 |
i18n - docs translations (#23179)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23179?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
eddb56d26e |
Fix uneven padding in the onboarding trust badge (#23174)
The "+10k" trust badge on the onboarding import contacts step had 2px left padding vs 10px right, so the PwC logo hugged the left edge. The badge now has equal padding on both sides so its content is centered. <img width="347" height="168" alt="image" src="https://github.com/user-attachments/assets/05e18541-827e-4ace-99ce-7e8c4201cd1b" /> |
||
|
|
de3889e04a |
fix: use aria-current instead of aria-selected on navigation drawer items (#23160)
## Fixes Closes #23129 ## Problem Navigation drawer items render an `<a>` (or React Router `Link`) with `aria-selected="true"`. `aria-selected` is only valid on roles such as `option`, `tab`, `row`, or `gridcell`, not on links, so axe flags WCAG 4.1.2 (aria-allowed-attr): "ARIA attribute is not allowed: aria-selected=true". ## Fix Use `aria-current="page"` instead. It is the WAI-ARIA recommended way to mark the current item in a navigation, it is a global attribute valid on any role (link, button, or div), and it correctly communicates the active page to screen readers. `packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx`: - aria-selected={active} + aria-current={active ? 'page' : undefined} <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23160?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
9043ab3091 |
Upgrade to 1.0.9 PDL app (#23172)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23172?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
e0debf87a7 |
fix(call-recorder): listen proper updated fileds event (#23135)
## Context Around meeting-end peaks (~6pm), Recall/Svix delivers event bursts for every recorded call across the 700+ workspaces the app is installed on. Each delivery was processed synchronously in the API request path, and internal failures surfaced to Svix as non-2xx, so it redelivered — a self-feeding storm of 500s and latency that only stopped when the webhook endpoint was disabled. ## What changed With #23134, server-route dispatch defaults to **queued** server-side: the API acks Svix with a 202 right after signature verification, `process-recall-webhook` runs on the worker queue, and failed runs retry there (resolver `retryLimit`, default 3). The resolver needs no change at all — `recall-webhook.ts` is back to main, and no SDK update is required. Remaining app changes: - `schedule-recall-bot-on-call-recording-update` declares `updatedFields` (the pending-transition fields) on its `callRecording.updated` trigger, so the server drops the app's own scheduling-progress and artifact writes **before** spawning a full execution instead of executing and returning "skipped". The in-handler check stays as a fallback. - Version bumped to 1.5.0. - Code comments introduced by earlier revisions of this PR removed per review. ## Tests - New test pins the trigger's `updatedFields` declaration. - `yarn test:unit` (507 tests), `yarn lint`, `yarn typecheck` all green. ## Notes - The `call-recorder (dockerhub-latest)` CI leg fails because main already requires `twenty >= 2.23.0` while the latest published image is 2.22.0 — pre-existing, clears when 2.23.0 images publish. - The 250s `import-call-recording-artifacts` route still runs in an API request slot behind the fire-and-forget own-route POST; moving it fully off the request path is a follow-up. |
||
|
|
f59bda1dbf |
feat(server): queued-only server-route dispatch (#23134)
## Context
Follow-up to the incident where 500s and latency spiked around 6pm until
the Recall webhook was disabled. Server routes
(`/webhooks/server/:resolverUid`) ran the resolver **and** the target
logic function synchronously inside the API request, so any handler
throw became a 500 and any slowdown past Svix's delivery timeout marked
the delivery failed — Svix redelivered, feeding load back into the API
in a self-sustaining storm.
## What changed
- Every server-route request acks with **202 `{ queued: true }`** as
soon as the resolver returns; the target runs on `logicFunctionQueue`.
Signature verification stays synchronous in the resolver and still
rejects with a non-2xx. External senders never observe target latency or
failures.
- The resolver contract is unchanged from main: `{ workspaceId,
targetLogicFunctionUniversalIdentifier, payload? }`.
- The target lookup still happens synchronously before enqueueing,
scoped to the resolver's application registration, so unknown targets
404 as before.
- Endpoints whose caller must read the response body (challenge
handshakes, Slack commands) should use `httpRouteTriggerSettings`
routes.
Final diff is 4 files: the server-route service, its spec, the
integration spec, and the docs page. Trigger jobs, fan-out, message
queue, shared types, and SDK are all untouched.
## Tests
- `server-route-trigger.service.spec.ts`: 202 ack + enqueue,
unknown-target 404 without enqueue, resolver auth/contract/error
mapping.
- Integration: `server-route-trigger-authorization.integration-spec.ts`
asserts the 202 queued ack (run locally against a seeded DB, green).
- `typecheck` + `lint:diff-with-main` + `oxfmt` clean.
## Notes
- **Breaking for existing server-route resolvers**: responses are always
202; the target's return value no longer reaches the caller. Existing
resolvers returning response bodies must move those endpoints to
`httpRouteTriggerSettings`.
- A queued target's handler failure is recorded in execution logs but
not retried (same as other queue-executed functions today); retry
semantics are deliberately out of scope here.
- Follow-up candidates: retry-on-failure semantics for queued
executions, `addBulk` for single-round-trip fan-out, declarative
signature verification to take resolver code out of the request path,
moving the call-recorder 250s artifacts import off the API request path.
- Companion PR #23135 (call-recorder): no app change needed for dispatch
— queued dispatch applies by default.
|
||
|
|
271f9f833b |
i18n - translations (#23171)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23171?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
0c964aee10 |
Fix blank pages and add error recovery in onboarding (#23069)
Onboarding showed a blank page in two places: before the plan/payment step, and briefly after the welcome animation. `ChooseYourPlan` returned `null` while its `ListPlans` query loaded, leaving the step empty during the crossfade. It now renders the step loader, and the plans query is warmed from an earlier onboarding step so the content is usually already there. The post-completion redirect lands transiently on `/`, whose route element was `<></>`, so the welcome animation could reveal an empty page. It now renders a skeleton, and the `null` Suspense fallbacks on payment-success and book-call are replaced too. A failed `ListPlans` query was worse than a blank frame: `PLAN_REQUIRED` redirects every route back to itself, so the user was locked out of the product with no way to retry. That step and the billing settings page now show a retryable error state. One related fix found on the way: `BlankLayout` had no error boundary, so a render-time throw anywhere in sign-in or onboarding took down the whole app. `DefaultLayout` already had one. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23069?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
16e7db8577 |
PDL react 19 (#23169)
Started to face in version 1.07 on staging 2.23.0 ``` Failed to load front component: Cannot read properties of undefined (reading 'ReactCurrentBatchConfig') ``` |
||
|
|
ec859faa6a |
i18n - translations (#23170)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23170?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
f5ab8a236c |
fix: add aria-label to record table selection checkboxes (#23147)
## Fixes Closes #23130 ## Problem On record tables, the select-all header checkbox and each row's selection checkbox render as `role="checkbox"` with no accessible name, failing WCAG 4.1.2 (aria-toggle-field-name). Screen-reader users cannot tell what the checkbox selects. ## Fix The shared `Checkbox` component already accepts and forwards `aria-label`, but the record-table callers weren't passing one. Added translated labels: - Row checkbox: `aria-label={t\`Select row\`}` in `RecordTableCellCheckbox.tsx` - Header checkbox: `aria-label={t\`Select all rows\`}` in `RecordTableHeaderCheckboxColumn.tsx` Both use `const { t } = useLingui()` from `@lingui/react/macro`, following the existing i18n convention in this module. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23147?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
b4aa323889 |
Record Board & Record Calendar Drag Drop dnd-kit rewrite. (#23071)
Closes: #23070 Record Board: https://github.com/user-attachments/assets/26df63e2-fbbd-4339-88b3-eed73af283b4 Record Calendar: https://github.com/user-attachments/assets/cb209545-695e-44a1-8dfb-a92b3c8650a6 ### Technical Inputs - Each column is a `Droppable`. This differs from other implementations (Record table header and Record board header), where only the gap between headers is `Droppable`. - Individual cards are `Sortable`. - Uses `DragOverlay` to display a cloned version of the dragged card along with a `+N` chip when dragging multiple cards. - Updated `DragDropColumnDropTarget` to handle `vertical` and `horizontal` orientation of drop target. - File name changes: - `DragDropColumnDropTarget` → `DragDropItemDropTarget` - `DragDropColumnDroppableSlot` → `DragDropItemDroppableSlot` - `DragDropColumnSortableCell` → `DragDropItemSortableCell` - `DragDropColumnSortableHandle` → `DragDropItemSortableHandle` - `DragDropColumnDndContext` → `DragDropItemDndContext` - `DragDropColumnData` → `DragDropItemData` <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23071?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
3223e3c58f |
fix - fix light overscroll flash in dark mode (#23153)
Before https://github.com/user-attachments/assets/7dc35369-0c1e-4ca8-9d30-12738c7e7d5b After https://github.com/user-attachments/assets/b8dd49c2-9ad3-49d0-a107-6ddfdf1ba531 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23153?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
0489d39f2b |
fix(messaging): stop group and bulk emails from creating contacts (#23137)
Group addresses were only checked against the message sender, so anything arriving as reply-to or cc slipped through and became a contact. Now checked per participant at contact creation. The group word list can't keep up with real senders (posts-recap@, showinfo@, follow-suggestions@), so bulk-mail headers back it up: `List-Unsubscribe, List-Id, Precedence, Auto-Submitted` (Industry standard headers) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23137?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
8d943e1f68 |
Bump to alpha 2 all published apps (#23165)
Related https://github.com/twentyhq/twenty/pull/23155 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23165?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
41c32a04e4 |
i18n - docs translations (#23162)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
25f28ee299 |
Fix TWENTY-SERVER-60Y: register permissions exception filter globally (#23104)
## Context Sentry [TWENTY-SERVER-60Y](https://twenty-v7.sentry.io/issues/6633503406) ("Permission Denied: Entity performing the request does not have permission") has ~12.9k occurrences / 151 users. It groups by the shared throw site `settings-permission.guard.ts:57`, so it's a **catch-all bucket** for settings-permission denials across many resolvers, not a single operation. Sampled events include `findOneApplication` (app runtimes reading their own `applicationVariables`), `uploadFilesFieldFileByUniversalIdentifier`, `UpdatePageLayoutWithTabsAndWidgets`, `CreateFileUpload`, etc. ## Root cause Settings-permission denials are only converted to a client-appropriate `FORBIDDEN` when a resolver manually attaches `PermissionsGraphqlApiExceptionFilter`. **28** GraphQL resolvers do; **~35** guarded resolvers do not. On those, the guard-thrown `PermissionsException` (a `CustomException`, not a `BaseGraphQLError`) falls through to the Yoga error hook, is serialized as `INTERNAL_SERVER_ERROR`, and `shouldCaptureException` reports it to Sentry as a 500-class error. REST is not affected: every `SettingsPermissionGuard` controller already carries `PermissionsRestApiExceptionFilter` (15/15). ## Fix Register `PermissionsGraphqlApiExceptionFilter` globally via `APP_FILTER` in the core and metadata engine modules, mirroring the existing global GraphQL filters `BillingGraphqlApiExceptionFilter` and `FlatEntityMapsGraphqlApiExceptionFilter`. The filter is guarded on the GraphQL context (`host.getType()`), so REST keeps its existing per-controller filter untouched and no request-scoped dependency is pulled into a global provider. Every settings-guarded resolver now returns `FORBIDDEN` for denials, which is both the correct client error code and excluded from Sentry. Existing per-resolver `@UseFilters(PermissionsGraphqlApiExceptionFilter)` entries remain valid (handler-scoped takes precedence, identical result); collapsing them into the global registration is a possible follow-up. ## Test Added a case to `granular-settings-permissions.integration-spec.ts`: a member without the `APPLICATIONS` flag calling `findOneApplication{applicationVariables{key value}}` (the exact Sentry query, and a resolver with **no** resolver-scoped filter) must receive `FORBIDDEN`, exercising the global filter. Verified against the local test DB: - With the global filter: passes (`FORBIDDEN`). - Without it: fails with `INTERNAL_SERVER_ERROR`, reproducing the leak. - The existing roles / workspace-members / api-keys denial tests (per-resolver filters) still pass, confirming no precedence conflict and no boot issue from dual `APP_FILTER` registration. Fixes TWENTY-SERVER-60Y |
||
|
|
623aac5a56 |
Show agent names on real estate dashboard charts (#23146)
Follow-up to the real estate demo app: the Agency Overview dashboard's "Listings by agent" and "Showings by agent" bar charts grouped by the agent relation, which rendered the agent's UUID on the axis. Adding `primaryAxisGroupBySubFieldName: 'name.firstName'` groups by the agent's first name instead, so the charts show agent names (Emma, Lucas, Chloe, Louis). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23146?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
3ee8b72aa3 |
twenty-sdk env var to disable prov check (#23155)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23155?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
9d4a564af4 |
i18n - translations (#23157)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23157?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |