dee653dfa6d21282ba3672cdbeffb0963c500e2e
5460 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dee653dfa6 |
fix: move message list member backfill command to 2.25 (#23271)
## What
Moves the `backfill-message-list-members-junction-target` workspace
upgrade command from `2.24.0` to `2.25.0`.
Introduced in #23176 (commit
|
||
|
|
ba5cb6ba15 |
fix(server): repair missing keyValuePair.applicationId on 2.23 upgrades (#23272)
Fixes #23254 ## Problem Upgrading a self-hosted instance from `2.23.x` to `2.24.0` leaves `core.keyValuePair` without the `applicationId` column. Database-backed config loading then fails on startup and on every refresh (~every 15s) with: ``` column KeyValuePairEntity.applicationId does not exist ``` The frontend shows "Unable to reach the backend". ## Root cause `AddApplicationIdToKeyValuePairFastInstanceCommand` was added in #23089 (after `2.23.x` shipped) but registered under the already-released `2.23.0` segment: ```ts @RegisteredInstanceCommand('2.23.0', 1784659343818) ``` The upgrade cursor is **positional and forward-only**: - `resolveStartCursor` resumes at `lastAttemptedIndex + 1`. A fully-upgraded `2.23.x` instance has its cursor at the last `2.23` workspace command, which sits *after* this newly-inserted fast command in the sequence. So the runner steps right over it and the DDL never runs. - The upgrade-aware metadata layer decides "applied" the same way (`stepIndex < currentCursor` in `upgrade-aware-entity-metadata.adapter.ts`). Since the step index is below the cursor, the column is considered applied and is **not** hidden from TypeORM SELECTs, so every query references a column that was never created. Fresh `2.24.0` installs replay the whole sequence, so only `2.23.x -> 2.24.0` upgrades are affected. The instance log `1 fast instance ... for 2.24.0` confirms the command landed in the `2.23.0` bundle rather than `2.24.0`. ## Fix - Add `RepairKeyValuePairApplicationIdFastInstanceCommand` under the current version (`2.24.0`) with a fresh timestamp, so it sorts last in the sequence and runs for every existing instance regardless of cursor position. Its DDL mirrors the original command and is fully idempotent (`ADD COLUMN IF NOT EXISTS`, `DROP INDEX IF EXISTS` + recreate, `ADD VALUE IF NOT EXISTS`), so it is a no-op on healthy instances. `down()` is intentionally empty: the column lifecycle is owned by the `2.23.0` introduction command. - Repoint the entity's `@WasIntroducedInUpgrade` to the new command so the column stays hidden from queries until the repair has actually run, eliminating the error window during the migration itself. ## Notes - `2.24.0` (`TWENTY_CURRENT_VERSION`) is the correct target: the upgrade sequence only covers previous + current versions, so a command under `2.25.0` (a next version) would not run. If a version bump lands before this merges, the command should be moved to the new current version. - Follow-up worth considering: nothing currently prevents registering a command under a version in `TWENTY_PREVIOUS_VERSIONS`. A startup validation rejecting that would have caught this at PR time. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23272?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. --> |
||
|
|
abe4d7491c |
Cap nested relation query concurrency (#23252)
## Context Common API queries load selected relations after fetching the root records. Relation loading is batched: one query pipeline loads a relation for all parent records, so this is not an N+1 problem. However, every sibling relation currently starts concurrently through `Promise.all`. Nested relations repeat the same behavior recursively. A wide selection can therefore submit many independent relation query pipelines at once. Existing query complexity and record limits restrict what can be requested, but they do not limit how much database work starts concurrently. ## What this changes This PR adds a request-local FIFO concurrency limiter for nested relation loading. - At most four `findRelations` pipelines execute concurrently. - One limiter is created for the outer relation-loading call. - The same limiter is shared by every recursive level. - Queued work starts as permits become available. - Permits are released in `finally`, including when a query fails. Conceptually: ```text Before: all sibling relations -> database concurrently nested siblings -> more database work concurrently After: all sibling relations -> FIFO queue -> at most 4 database pipelines nested siblings -> same FIFO queue and same limit ``` Note: Also addressing https://github.com/twentyhq/twenty/pull/23251#discussion_r3644510597 |
||
|
|
e5c9fcf058 |
Add PostgreSQL connection pool pressure metrics (#23251)
## Summary - Add pool gauges for total, idle, waiting, and maximum connections - Record PostgreSQL connection acquisition duration and failures - Instrument core, workspace primary, and optional replica data sources - Add unit tests covering gauges, acquisition timing, failures, and deduplication <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23251?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. --> |
||
|
|
d0863dd1f7 |
i18n - translations (#23253)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
1fdb5605f1 |
feat: kanban, calendar and grouped-table layouts for relation field widgets (#23112)
## Context A relation field widget on a record page can already embed a record-scoped view rendered as a **table** (`FieldDisplayMode.TABLE`) — e.g. a Company's Opportunities. This brings **kanban, calendar and grouped-table** to that same embedded view, so the board/calendar stays scoped to *this* record's related records (not a standalone all-records widget — that was the earlier #23003 approach, closed). Builds directly on the merged dashboard widget layouts (#22963), reusing its renderer, draft/save pipeline, and settings dropdowns. ## Approach — extend the existing "Table" display mode The relation field widget already stores a `viewId` and renders it through the layout-agnostic `RecordTableWidgetRendererContent` (which branches on the embedded view's `type`), scoped to the current record via `RecordFilterValueDependenciesContext`. So rendering + persistence already work for any widget view type — only the authoring UI and one server gate were missing. **No new `FieldDisplayMode`, no data migration.** ## Server - `view-widget-upsert.service.ts`: a field widget in table display mode (`isFieldTableWidget`) could already persist viewFields/filters/sorts through this path, but was **blocked from updating view settings** (`type` / group-by / calendar), pinning its embedded view to a table. The widget-type guard earlier in the method already rejects every widget kind other than record-table and field-table, so the now-redundant record-table-only guard on the view-settings branch is dropped. The allowed-widget-view-types check and the downstream group-by / calendar-field validations still apply equally. ## Frontend - **One merged Layout picker.** The field widget's Layout dropdown lists **Field / Card / Table / Kanban / Calendar** in a single flat list — you pick Kanban directly, instead of "Display as: Table" first and a separate embedded-view layout second. Picking a view layout selects the `TABLE` display mode under the hood, seeds the record-scoped embedded view on first use (with a default group-by / date field), and applies the layout in the same click. Kanban/Calendar are disabled with a hint ("Needs a Select field" / "Needs a Date field") when the relation target can't support them — same gating as the dashboard picker. The row's icon and description reflect the effective selection (e.g. Kanban), and the dropdown mounts the draft-init effect so switching straight from Field/Card to Kanban works before the table renderer has ever mounted. - **Contextual rows** (Group by / Date field / Calendar view / Hide empty groups) extracted from the dashboard panel into a reusable `WidgetViewLayoutSettingsRows` (source object passed in — fixed to the relation target; no Source / Limit rows) and surfaced under the picker while a view layout is active. Its standalone layout row is hidden here (`isLayoutRowHidden`) since layout lives in the merged picker. - Reuses the dashboard draft snapshot + `upsertViewWidget` save pipeline and the group-by/calendar dropdown components unchanged. ## Scope - **One-to-many relations only** (matches the existing `getFieldWidgetAvailableDisplayModes` gate; junction / many-to-many stay table-only — a pre-existing inconsistency left untouched here). - Field-widget **calendars inherit the dashboard's behavior** (month read-only by default; day/week + drag-to-reschedule only behind `IS_CALENDAR_WEEK_VIEW_ENABLED`), since it's literally the same renderer. ## Tests - Server integration (`upsert-view-widget-view-settings.integration-spec.ts`): a FIELD + TABLE widget can switch its embedded view to `KANBAN_WIDGET` (with group-by) and `CALENDAR_WIDGET` (with date field), and the kanban group-by validation still applies through the newly-opened path. - Front unit: `getWidgetViewLayoutSettingsItemIds` (keyboard-nav row ids per layout/flag/group state). ## Follow-ups (intentionally not in this PR) - Migrate the dashboard settings panel onto the shared `WidgetViewLayoutSettingsRows` (kept out to avoid churning the just-merged #22963 file; behavior-preserving refactor). https://claude.ai/code/session_01E5N87kwwZWhDtEQaP72cMf |
||
|
|
fdb8865933 |
test: cover uninstall logic function hook execution (#23249)
Follow-up to #23227 ([review comment](https://github.com/twentyhq/twenty/pull/23227#pullrequestreview-4771629391)): adds integration coverage for the `defineUninstallLogicFunction` hook execution on uninstall. ## What New integration suite `successful-uninstall-application-logic-function-hook.integration-spec.ts` that drives the real `syncApplication` / `uninstallApplication` GraphQL flow and asserts on the executor wiring: - When the synced manifest declares an `uninstallLogicFunction`, uninstalling the application resolves the hook and calls `LogicFunctionExecutorService.execute` exactly once, before deletion, with the `{ version }` payload. - When the manifest declares no uninstall hook, uninstalling the application does not call the executor. The executor is spied via the running app container (`getAppProviderByClassName`) and stubbed to a success result, so the test verifies the server-side resolution/trigger path deterministically without depending on the local function runtime. ## Test plan - `npx jest --config ./jest-integration.config.ts successful-uninstall-application-logic-function-hook` passes (2/2). - Typecheck green for twenty-server; oxlint and oxfmt clean on the new file. --- _Generated by [Claude Code](https://claude.ai/code/session_016zJPggkVEw1V7SqnPSiUQx)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23249?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: martmull <martin@twenty.com> |
||
|
|
f793f3c5a9 |
Fix application logos resolving to null on install and sync (#23245)
## Problem Application icons are no longer resolved: `logo` is `null` on `FindManyApplications` / `FindOneApplication`, so app chips and the settings applications table fall back to initials avatars. ## Root cause Manifests produced by the current SDK carry the logo path in `manifest.application.logo`; `logoUrl` is deprecated and stripped by `normalizeApplicationAssets` (external URLs are dropped, relative ones are moved to `logo`). Three server call sites still read only the deprecated `logoUrl`: - `application-sync.service.ts` (`syncApplication`): wrote `logo: manifest.application.logoUrl ?? null` on every install/upgrade/dev sync, overwriting `application.logo` with `null` - `application-sync.service.ts` (`buildVirtualDryRunFlatApplication`): same read on the dry-run path - `application-install.service.ts` (`ensureApplicationExists`): same read on the create path Since both `Application.logo` and the `Application.logoUrl` resolve field derive from that column, icons went null everywhere. Separately, OAuth-only apps (e.g. "Twenty CLI" from dynamic client registration) never get a logo at all: `OAuthRegisterInput` accepts `logo_uri` but the registration controller dropped it, so `applicationRegistration.logoUrl` also resolves to null for those. ## Fix - Read `manifest.application.logo ?? manifest.application.logoUrl ?? null` at all three sites, matching the fallback already used by `importLogoFile` and `fromManifestApplicationToDisplayFields` - Persist `logo_uri` into `applicationRegistration.logo` on OAuth dynamic client registration; `buildLogoUrl` passes absolute URLs through, so the consent screen and app chips can resolve it Existing rows that were already nulled will self-heal on the next app upgrade/sync, since the sync path rewrites `logo` from the manifest. --- _Generated by [Claude Code](https://claude.ai/code/session_01Us7BE5yBguYTACg5H3Y85z)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23245?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. --> |
||
|
|
abc82d66b7 |
Consolidate per-queue worker tuning in one explicit config file (#23229)
## Context
Follow-up to the worker configuration analysis and to the worker pool
split rolled out in twentyhq/twenty-infra#805/#806. Worker tuning was
previously spread across two partial constants (`QUEUE_WORKER_OPTIONS`,
`MESSAGE_QUEUE_PRIORITY`), and most queues silently relied on implicit
BullMQ defaults.
## What this PR does
Introduces a single dedicated file to pilot worker behavior per queue:
`src/engine/core-modules/message-queue/message-queue-worker-config.constant.ts`
`MESSAGE_QUEUE_WORKER_CONFIG` declares, for **every** queue, an
explicit:
- `priority` (applied when enqueuing, lower runs first)
- `concurrency`
- `lockDuration`
- `maxStalledCount`
- `boundedShutdownDrain`
Explicitness is enforced at compile time: the record is typed
`Record<MessageQueue, { priority: number; workerOptions:
Required<MessageQueueWorkerOptions> }>`, so adding a queue without
declaring its full configuration is a type error, and no field can be
omitted.
Wiring changes:
- `message-queue.explorer.ts` passes
`MESSAGE_QUEUE_WORKER_CONFIG[queueName].workerOptions` when creating
workers
- `bullmq.driver.ts` reads the enqueue priority from the same record
- `message-queue-worker-options.constant.ts`,
`message-queue-priority.constant.ts` and
`ai-stream-lock-duration.constant.ts` are removed (the AI stream lock
duration is inlined into the one config entry that used it)
## Behavior
No behavior change — the previously implicit BullMQ defaults
(concurrency 1, lockDuration 30s, maxStalledCount 1) are now spelled out
per queue, and the existing overrides (`ai-stream-queue`: concurrency 20
/ 10 min lock / no stall retry / bounded shutdown drain;
`logic-function-queue`: concurrency 10) and all priorities are carried
over unchanged.
## Validation
- `npx nx typecheck twenty-server` ✅
- `oxlint --type-aware` + `oxfmt --check` on changed files ✅
- `npx jest "message-queue"` (7 tests) ✅
Companion infra PR: twentyhq/twenty-infra#808 moves the worker pool
topology (replicas, resources, queue filters) into a dedicated
`workers.yaml` per environment.
Session: https://claude.ai/code/session_01TL6Te48Lkys5NxyG9j2Nz6
|
||
|
|
fb52635d2a | Add defineUninstallLogicFunction hook for applications (#23227) | ||
|
|
7f1e3d3541 |
fix(admin): prevent fallback to 'latest' string when dockerhub tags c… (#22885)
Fixes #22849 ### What changed? When the `AdminPanelVersionService` hits a DockerHub API error (or filters out all valid tags), it was previously hardcoded to return `'latest'`. This caused the frontend to display `Latest version: latest`. I updated the GraphQL DTO to make `latestVersion` nullable, and modified the service fallback and unit tests to return and expect `null` instead. The frontend (`SettingsAdminVersionDisplay`) already has logic to handle a falsy version and gracefully display `No latest version found`, so this backend fix entirely resolves the UX issue without touching the frontend. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22885?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. --> |
||
|
|
148dc6dfaa |
Let server route resolvers answer the caller synchronously (#23233)
## Problem
A server route resolver can only return a dispatch target (`{
workspaceId, targetLogicFunctionUniversalIdentifier, payload }`), and
`ServerRouteTriggerService` always acks `202 {queued:true}`. The target
function runs off the queue, after the response has been sent, so its
return value can never reach the caller.
That makes it impossible to integrate a provider whose webhook URL has
to be proven with a handshake on the same response. Slack's Events API
is the case that surfaced it: `url_verification` sends `{ type,
challenge }` and will not accept the Request URL unless the challenge
comes back on that POST.
## Change
A resolver may now return a `Response` (the existing
`LogicFunctionHttpResponse`) instead of a dispatch target. The route
sends it as-is via `buildRouteTriggerResponse` and enqueues nothing.
- Reuses the marker and builder that HTTP route triggers already use, so
there is no new response shape.
- Dispatch results behave exactly as before; the resolver error path is
unchanged, just hoisted out of `parseResolverResult` so it runs before
the branch.
- SDK: `ServerRouteResolverResult` becomes `ServerRouteDispatchResult |
LogicFunctionHttpResponse`.
Additive: a resolver that returns a dispatch target sees no behavior
change. Previously, returning this shape threw
`RESOLVER_INVALID_RESULT`.
## Testing
`server-route-trigger.service.spec.ts` gains a case asserting the
resolver's response is sent verbatim and nothing is enqueued. 15/15
pass.
## Context
Split out of #22984 (Slack conversational assistant), which needs this
to complete the Slack Events URL verification. That PR depends on this
one merging first.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23233?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. -->
|
||
|
|
5bc97f8591 |
chore: sync AI model catalog from models.dev (#23242)
Automated daily sync of `ai-providers.json` from [models.dev](https://models.dev). This PR updates pricing, context windows, and model availability based on the latest data. New models meeting inclusion criteria (tool calling, pricing data, context limits) are added automatically. Deprecated models are detected based on cost-efficiency within the same model family. **Please review before merging** — verify no critical models were incorrectly deprecated. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23242?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: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com> |
||
|
|
34c2e11dcb |
i18n - translations (#23230)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23230?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> |
||
|
|
2c79093b74 |
feat: agent roleUniversalIdentifier for manifest-driven role assignment (#23206)
## Summary - Adds optional `roleUniversalIdentifier` on `AgentManifest` / `defineAgent` so apps can declaratively assign a role to an agent (same config shape as `defaultRoleUniversalIdentifier`). - Wires `agentUniversalIdentifier` as a sync many-to-one FK on `roleTarget`, and emits a deterministic `roleTarget` from the agent during app sync (create / update / delete). - Enables app agents (e.g. Slack assistant) to get a role on install without postInstall hooks or manual admin assignment. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23206?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. --> |
||
|
|
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. --> |
||
|
|
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> |
||
|
|
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. --> |
||
|
|
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. --> |
||
|
|
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. --> |
||
|
|
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. --> |
||
|
|
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> |
||
|
|
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. --> |
||
|
|
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. --> |
||
|
|
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.
|
||
|
|
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. --> |
||
|
|
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 |
||
|
|
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> |
||
|
|
4c4a154d31 |
key-value storage for applications (#23089)
## What
Key-value storage for applications, as proposed in
twentyhq/core-team-issues#2391 — built on the existing `keyValuePair`
entity:
- Nullable `applicationId` relation on `keyValuePair` + a new
`APPLICATION_VARIABLE` type (fast instance command included)
- GraphQL CRUD on the metadata schema (`appKeyValue`, `setAppKeyValue`,
`deleteAppKeyValue`), requiring an `APPLICATION_ACCESS` token —
`applicationId` always comes from the token, never from arguments, so
apps can't touch each other's entries
- `kv.get` / `kv.set` / `kv.delete` helpers in
`twenty-sdk/logic-function`
## Scopes
- **`INSTALL`** (default): entries are private to one workspace install;
arbitrary JSON values
- **`GLOBAL`**: entries are shared across every install of the app, with
claim semantics — the value is always the claiming `workspaceId` and
only that workspace can overwrite or delete the key (guarded writes,
race-safe via insert-if-absent)
Since `applicationId` identifies an install (one row per workspace),
GLOBAL entries are stored under the registration owner workspace's
install so all installs of the same app share one namespace.
The GLOBAL scope is what enables cross-workspace webhook routing: e.g.
the Slack app's `serverRoute` resolver (running in the owner workspace)
can resolve `kv.get('slack:team:' + team_id, { scope: 'GLOBAL' })` to
find the workspace that connected that Slack team — without a workspace
being able to hijack another's mapping.
## Follow-ups
- Wire the Slack assistant PR (#22984) to write the claim at connect
time and read it in the events resolver
- `kv.*` access from front components
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23089?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. -->
|
||
|
|
10a313dc7f |
chore: bump version to 2.24.0 (#23152)
## 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/23152?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> |
||
|
|
893db7558e |
Fix instance fast migration, fallback index creation (#23149)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23149?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. --> |
||
|
|
7fa3c5c77b |
chore: sync AI model catalog from models.dev (#23143)
Automated daily sync of `ai-providers.json` from [models.dev](https://models.dev). This PR updates pricing, context windows, and model availability based on the latest data. New models meeting inclusion criteria (tool calling, pricing data, context limits) are added automatically. Deprecated models are detected based on cost-efficiency within the same model family. **Please review before merging** — verify no critical models were incorrectly deprecated. Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com> |
||
|
|
984944e9bc |
Fix workspace cache flush versioned metadata (#23121)
## Problem
`WorkspaceCacheStorageService.flushVersionedMetadata` never deleted
several of the keys it was supposed to flush:
- When called without a `metadataVersion` (the dev seeder path), it
built keys ending in `:*` and passed them to `cacheStorageService.del`.
`del` is an exact-key delete with no glob support, so this branch
deleted nothing at all.
- `setGraphQLTypeDefs` and `setGraphQLUsedScalarNames` can write
applicationId-suffixed keys
(`{key}:{workspaceId}:{metadataVersion}:{applicationId}`), which the
flush loop never matched. On workspace deletion these SDL and scalar
entries survived in Redis until the 1-week TTL expired.
- The `MetadataVersion` key is stored without a version suffix
(`metadata:workspace-metadata-version:{workspaceId}`), but the flush
loop appended `:{metadataVersion}` to every key, so it never matched
either.
## Fix
`flushVersionedMetadata` now targets the key shapes that are actually
written:
- The `MetadataVersion` key is deleted by its exact, unversioned shape.
- With a known `metadataVersion`, each versioned key gets an exact `del`
on `{key}:{workspaceId}:{metadataVersion}` plus a `flushByPattern` on
`{key}:{workspaceId}:{metadataVersion}:*` to catch
applicationId-suffixed entries. The pattern requires the trailing colon
so flushing version 1 cannot match version 12.
- Without a `metadataVersion`, each versioned key is flushed with
`flushByPattern` on `{key}:{workspaceId}:*`.
`flushByPattern` is Redis-only, which is safe here: the cache module
factory hardcodes the Redis store, and `flushGraphQLOperation` in the
same service already relies on it.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23121?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. -->
|
||
|
|
5add5ea695 |
fix(billing) - expand billing sub uniqueness constraint (#23123)
fixes : https://discord.com/channels/1130383047699738754/1528956737363771497 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23123?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. --> |
||
|
|
af6f3c0c06 |
i18n - translations (#23126)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
28adcbffb9 |
Remove dead code from legacy metadataVersion cache mechanism (#23122)
- Drop unused setORMEntitySchema/getORMEntitySchema from WorkspaceCacheStorageService - Drop MetadataObjectMetadataMaps cache key, only referenced by the flush loop - Drop never-populated workspaceMetadataVersion field from workspace auth context type and builders - Drop unthrown TwentyORMExceptionCode.METADATA_VERSION_MISMATCH - Drop unused WorkspaceMetadataVersionModule imports in field-metadata and object-metadata modules <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23122?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. --> |
||
|
|
578d69e4fb |
Fix update events reporting untouched columns as changed (#22797)
## Before https://github.com/user-attachments/assets/b02dbd50-87c9-4699-a6a9-254a4c8b9182 The image uploaded during the onboarding is deleted and doesn't appear in the animation ## After https://github.com/user-attachments/assets/9443b837-044e-47c6-b2df-c392c238e935 The Image appears in the animation ## Description TwentyORM's `.save()` emits UPDATE events whose `after` record carries default (empty) values for columns that weren't written: TypeORM null-injects untouched nullable columns on the entity in place, and `formatResult` turns those into empty strings. So a partial update (e.g. renaming a workspace member) reports untouched fields like `avatarUrl` and `userEmail` as changed, which is what made the avatar-file-deletion listener delete the picture uploaded during onboarding. The same stale data also reaches webhooks, workflow/logic-function triggers and record subscriptions. Fix: `save()` was the only write path building its event `after` from the in-memory payload instead of re-reading the row. `update()`, `upsert()` and `softDelete()` all re-SELECT after the write, so `save()` now does the same. `withDeleted` is on both the before and the after find, since `save({ id, deletedAt })` and `save({ id, deletedAt: null })` are valid soft-delete and restore, and an asymmetric find would leave a restored row with no matching before-record. Worth noting: a genuinely no-op save now emits no update event, where it previously emitted one with a bogus diff. |
||
|
|
23cf85f745 |
Fix invalid UUID insert in workflow core-links backfill (#23118)
## Fix invalid UUID insert in workflow core-links backfill ### Problem The `2-23:backfill-workflow-core-links` workspace upgrade command failed with: ``` QueryFailedError: invalid input syntax for type uuid: "" (22P02) ``` The `core."workflow"."lastPublishedVersionId"` column is a `uuid`, but some workspace workflows store an empty string `""` (not `NULL`) for that field. The code used `workflow.lastPublishedVersionId ?? null`, and `??` only falls back on `null`/`undefined` — so `""` was passed straight through and Postgres rejected it. ### Fix Use `|| null` instead of `?? null` so empty strings are normalized to `null` before insertion into the `uuid` column. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23118?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. --> |