830404b215212be1423f5a6dfbc080c60a699f02
4229 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
830404b215 |
fix: Decrypt encrypted front component variables (#23494)
## Summary Fixes #23492 Fixes front-component application variables returning their encrypted at-rest value instead of their configured plaintext value. Non-secret application variables (`isSecret: false`) are now decrypted server-side before being injected into the front-component environment. Secret variables remain excluded and are never decrypted or exposed to the browser. ## Root cause The front-component resolver filtered secret application variables correctly, but forwarded the cached `encryptedValue` directly. As a result, `getApplicationVariable()` returned an `enc:v2:...` envelope rather than the configured value. ## Changes - Decrypt recognized versioned envelopes for non-secret application variables. - Preserve empty and legacy/plain values unchanged for backwards compatibility. - Add `SecretEncryptionModule` to the front-component module. - Add coverage for: - decrypting public variables; - retaining plaintext compatibility; - excluding secret variables without attempting decryption. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23494?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: prastoin <paul@twenty.com> |
||
|
|
a99e62fbca |
Add application job enqueue limits guard on all message queue drivers (#23570)
## Context
Applications trigger logic functions from several paths (cron, database
events, HTTP routes, install/connect hooks). Without a cap, a single app
can flood the `logic-function-queue` and starve it. This adds enqueue
limits scoped to that queue, mirroring the existing per-application API
rate limiting.
## What changed
`JobEnqueueThrottlerGuard` reuses the `ThrottlerService` token bucket
(same primitive as the API rate limiter) with two tiers:
- **Per application installation**
(`enqueue:throttler:application:{applicationId}`) - lower ceiling,
`APPLICATION_JOB_ENQUEUE_RATE_LIMITING_LIMIT` (default 500).
- **Per application registration**
(`enqueue:throttler:application-registration:{applicationRegistrationId}`)
- higher ceiling shared across all workspaces that installed the same
app, `APPLICATION_REGISTRATION_JOB_ENQUEUE_RATE_LIMITING_LIMIT` (default
2000).
Both share `APPLICATION_JOB_ENQUEUE_RATE_LIMITING_TTL_IN_MS` (default
60s). Both buckets are checked before either is debited, so a rejection
on one tier never burns quota on the other.
**Per-queue guarding.** A `ThrottledMessageQueueDriver` decorator wraps
the concrete driver (BullMQ or Sync) in the `QUEUE_DRIVER` provider and
passes the `queueName` to the guard. The guard only acts on queues in
`GUARDED_ENQUEUE_QUEUES` (currently just `logic-function-queue`); every
other queue is untouched.
**Required application context.** The guard reads a dedicated
`applicationJobEnqueueContextStorage` (AsyncLocalStorage) carrying `{
applicationId, applicationRegistrationId }`, and throws if a
guarded-queue enqueue runs without both. Every logic-function-queue
enqueue site wraps its `add`/`bulkAdd` in
`withApplicationJobEnqueueContext`:
- cron trigger
- database-event trigger (groups logic functions by application, one
batch per application)
- server route trigger
- application post-install hook
- connection-provider on-connect hook
When the limit is reached the guard records a
`JobEnqueueApplicationRateLimited` metric and throws
`ThrottlerException` (mapped to 429 by the existing handlers).
## Files
- `message-queue/guards/job-enqueue-throttler.guard.ts` - the guard
(new)
- `message-queue/storage/application-job-enqueue-context.storage.ts` -
dedicated enqueue context (new)
- `message-queue/constants/guarded-enqueue-queues.constant.ts` -
guarded-queue set (new)
- `message-queue/drivers/throttled-message-queue.driver.ts` - decorator
driver wrapping any driver (new)
- `message-queue/message-queue-core.module.ts` - wires the guard into
the driver provider
- `twenty-config/config-variables.ts` - three tunable `RATE_LIMITING`
config variables
- `metrics/types/metrics-keys.type.ts` -
`JobEnqueueApplicationRateLimited` key
- the 5 enqueue sites above - inject the enqueue context
## Notes / trade-offs
- A logic function whose application has no `applicationRegistrationId`
is skipped at the trigger paths (the install hook throws), matching how
the server route trigger already treats "not linked to a registration".
- `addCron` is left ungated (idempotent upsert).
- Default limits are placeholders and tunable per instance.
## Testing
- `JobEnqueueThrottlerGuard` unit tests (7 cases): non-guarded queue
skip, throw on missing/partial context, two-tier throttling with
distinct limits, per-item token consumption on bulk, no partial debit
when either tier is exhausted.
- Updated `connection-provider-oauth-flow.service.spec.ts` for the new
cache key.
- `npx nx typecheck twenty-server` passes; oxlint + oxfmt clean on
changed files.
|
||
|
|
bd65dbd47a |
Cache metadata lookups during ORM result formatting (#23593)
## Context Production profiling identified `formatResult` as a recurring CPU hotspot on read paths, especially for list queries and nested relations. The formatter receives one metadata snapshot for the complete result, but previously rebuilt metadata-derived lookup structures for every record. For each record, including recursively formatted relation records, it rebuilt or rescanned: - field name and join-column maps - composite field property maps - required composite properties - date and date-time field collections This metadata does not change while one result is being formatted, so the repeated work scaled with the number of records without changing the output. It also created short-lived allocations that added GC pressure on busy server pods. ## What changed - Create a private cache for each top-level `formatResult` invocation. - Lazily derive formatter metadata once per object metadata ID. - Reuse it across records in an array and recursively formatted relations. - Precompute required composite property names and date-time field metadata. - Remove the DATE post-processing pass, which assigned each value back to itself. - Keep the exported `formatResult` signature unchanged. The cache is discarded when the formatting call returns. ## Why use a call-scoped cache The derived structures are valid for the metadata maps passed to one `formatResult` call. Keeping the cache local provides reuse for the complete result batch without adding cross-request state, invalidation rules, or another long-lived memory cache. This also preserves existing callers and keeps recursive implementation details private. ## Safety - Formatting behavior and returned shapes are unchanged. - Nested relation formatting still resolves metadata for each target object type. - Composite null and default handling, and DATE_TIME validation, are unchanged. - Metadata is recomputed for every top-level invocation, so a later request cannot reuse data derived from an older metadata snapshot. - No Redis, workspace-cache, database, or public API behavior changes. ## Expected impact Metadata preparation now scales with the number of object types in a result instead of the number of records. The largest benefit is expected for list queries and nested relations, with lower CPU usage and fewer short-lived allocations. This is a targeted result-formatting optimization. It does not address every source of API tail latency or retained cache memory. ## Validation - Added a nested-relation regression test that verifies unchanged output. - The test verifies metadata resolution is bounded per object type within one invocation and recomputed for a separate invocation. - Focused formatter Jest suite. - Existing chart relation-label Jest suite, 10 tests. - Type-aware Oxlint. - Oxfmt. - `yarn nx typecheck twenty-server`. |
||
|
|
bc0ec6b104 |
Maintain INDEX view system side effects on deactivated views (#23590)
# Introduction Follow-up on https://github.com/twentyhq/twenty/pull/23585#discussion_r3683701472. Two INDEX view side-effect handlers bailed out when the view had `isActive: false`. This drops those gates. # Why `isActive: false` on a view has exactly one writer: the delete path, when `isCallerOverridingEntity` is true (`from-delete-view-input-to-flat-view-or-throw.util.ts`, `view.service.ts`). It is not in `FLAT_VIEW_EDITABLE_PROPERTIES`, so nothing else sets it. So the flag means "the workspace deleted an engine-owned view, and since the engine owns the row we deactivate instead of hard-deleting". It is a workspace override of a row we still own, not a signal the row is gone (that is `deletedAt`). Which makes it precisely the state where the engine must keep maintaining its own rows: the row still exists, still belongs to the engine, and is expected to be consistent whenever the override is lifted. Skipping the side effect instead left the view permanently incomplete, with no repair path. The gates were inherited from the candidate-view scan removed in the same commit as these handlers were introduced (`compute-flat-view-fields-from-fields-widgets.util.ts`, #23081). There, `!view.isActive` filtered which of many views were candidates. Transplanted into handlers that resolve *the* one deterministic engine-owned INDEX view identifier, the same predicate stops meaning "is this a candidate" and starts meaning "silently skip the system side effect". # Changes - `fieldIndexViewFieldOnCreate`: create the INDEX view field even when the view is deactivated. - `objectIndexViewLabelIdentifierOnUpdate`: reconcile the label identifier view field even when the view is deactivated. `deletedAt` gates are unchanged in both. The `should noop when the object has no active INDEX view` spec case is inverted accordingly. # Follow-up Both handlers also drop inactive view fields when computing positions, while `FlatViewFieldValidatorService` builds its `otherFlatViewFields` with no `isActive` filter. An inactive view field below all active ones would make a handler emit a label identifier position the validator then rejects. Unreachable today (view fields are only ever soft-deleted, never deactivated), so left out of this PR. |
||
|
|
08891db8be |
Create INDEX view fields visible on field creation (#23585)
# Introduction Follow-up on https://github.com/twentyhq/twenty/pull/23081. Creating a field on an existing object added its column to the object's index view hidden, while creating the same field alongside its object added it visible. Same field, different outcome depending on when it was created. Both now create it visible. Hiding the column stays one click away, and that choice is kept as a user override on top of the engine default. Applies to fields created from now on. Nothing is backfilled: an already hidden column cannot be told apart from one a user hid on purpose. `objectSystemFieldsAndIndexViewOnCreate` and the 2-26 reconcile command are untouched. |
||
|
|
5848c9bd30 |
Display object, field and view links as chips in the AI chat (#23573)
<img width="3840" height="1876" alt="CleanShot 2026-07-30 at 15 37 46@2x" src="https://github.com/user-attachments/assets/9fe178b9-c2fa-4b05-9c9d-0cdc80270b67" /> https://github.com/user-attachments/assets/34c4e486-c462-4300-ae98-da99f614f069 The AI chat already renders record chips from a `[[record:...]]` marker the model writes in its prose, but naming an object, field or view produced plain text. This adds three sibling markers so those render as chips too, as in the [Figma design](https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=104416-116261). - `[[object:<nameSingular>:<label>[[/object]]` links to the record index page. It is name-keyed rather than id-keyed so an object the assistant only *proposes* to create still renders as a chip, just without a link. - `[[field:<id>:<label>[[/field]]` links to the field's settings page, gated on the `DATA_MODEL` permission. - `[[view:<id>:<label>[[/view]]` links to the object index page for that view. Field and view ids must come from a tool, so an unresolvable one falls back to plain text rather than a chip that goes nowhere. The record-only parser becomes one scan over all four kinds. Alternative order is load-bearing: `[[view:<uuid>:` is shaped exactly like the legacy prefix-less record marker, so metadata kinds are tried first and only records keep the legacy `]]` terminator. Server side is prompt-only. The metadata and view tools return bare objects rather than `ToolOutput`, so there is nowhere to hang a structured reference array without wrapping every factory, and the names and ids the markers need are already in those results verbatim. Also fixes a pre-existing issue in `LazyMarkdownRenderer`: its `components` map was rebuilt on every render, and react-markdown uses each entry as the JSX element type, so every node remounted on every streamed chunk. Harmless before, expensive once the model is told to chip every metadata name it writes. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23573?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. --> |
||
|
|
3689e89440 |
Optimize upgrade status gauges with count-only queries (#23574)
## Context Upgrade health metrics and the admin upgrade-status query currently share `getInstanceAndAllWorkspacesStatus`. On a cache hit, that method reads the cached behind/failed workspace IDs, then hydrates every workspace name with an individual `CoreEntityCacheService.get` call. This is useful for the admin response, but the gauges only need the number of workspaces in each state. As the number of behind or failed workspaces grows, every gauge refresh therefore creates a fan-out of entity-cache lookups. Those lookups can include Redis validation and response deserialization. Production profiling of slow upgrade-status requests showed `loadWorkspaceNamesById` and `CoreEntityCacheService.get` on the hot path, so this PR removes that unnecessary repeated work. ## What changed - Added a count-only upgrade-status method for metric collection. - Updated upgrade gauges to use cached ID counts without loading workspace names. - Replaced the admin path's per-workspace cache lookups with one repository query selecting only `id` and `displayName`. - Removed the upgrade module's now-unused core-entity-cache dependency. ## Why this improves performance ### Metrics path Before: - Read the cached upgrade-status IDs. - Run one entity-cache lookup per behind/failed workspace. - Discard the hydrated names and only use the array lengths. After: - Read the same cached upgrade-status IDs. - Derive counts directly from those IDs. - Perform no workspace-name lookup. **This changes metric collection from a fixed set of status-cache calls plus `N` entity-cache calls to only the fixed status-cache calls. The amount of ID data still scales with the number of affected workspaces, but the Redis/client round-trip fan-out does not.** ### Admin path The admin response still needs workspace names. It now loads them with one primary-key `IN` query instead of `N` independent entity-cache calls. This reduces round trips and repeated cache validation while preserving the response shape. ## Safety and behavior preservation - Upgrade-status cache keys, TTLs and invalidation behavior are unchanged. - A missing cache marker still triggers the existing full status refresh. - Metrics names and values are unchanged. - The admin GraphQL response is unchanged. - Cached workspace IDs missing from the database still produce a `null` name, matching the previous behavior. - The batched query runs only for callers that request the detailed admin payload, not for metric collection. ## Expected impact - Remove recurring per-workspace cache fan-out from every API process collecting upgrade gauges. - Reduce Redis client work, response deserialization and event-loop pressure during metric collection. - Reduce latency for detailed admin upgrade-status requests. This targets one profiled source of tail latency. It is not expected to eliminate all API p99 outliers, which also have independent causes. ## Validation - 36 focused upgrade-status and gauge tests pass. - `yarn nx typecheck twenty-server` passes. - Oxlint passes with zero warnings and errors. - Oxfmt and `git diff --check` pass. |
||
|
|
ad271ee639 |
Add connected account handle/provider index (#23580)
## Context
Google messaging webhook notifications resolve connected accounts with
an equality lookup on both `handle` and `provider`:
```ts
connectedAccountRepository.find({
where: {
handle: decodedData.emailAddress,
provider: ConnectedAccountProvider.GOOGLE,
},
});
```
This lookup runs for incoming Gmail notifications, but
`connectedAccount` currently has no index matching either predicate. As
the table grows, PostgreSQL has to inspect unrelated connected-account
rows for each notification. Under sustained webhook traffic, that adds
avoidable database work and keeps database connections occupied longer.
## What changed
- Add a composite B-tree index on `connectedAccount(handle, provider)`.
- Register the index in the TypeORM entity metadata.
- Add an idempotent 2.26 fast instance command to create the index for
existing installations and remove it on rollback.
The webhook handler and query behavior remain unchanged.
## Why this index
- Both query predicates are equality conditions, so the composite index
supports a targeted lookup.
- `handle` is first because it is the more selective value and also
makes the index useful for handle-prefixed lookups.
- The index is intentionally non-unique. The same provider handle may
legitimately belong to connected accounts in different workspaces, and
this change must not introduce a new data constraint.
- Connected accounts are read by webhooks much more frequently than
their handle or provider changes, so index maintenance overhead should
remain small.
## Expected impact
Webhook account resolution should use an index lookup instead of
scanning the connected-account table. This reduces cumulative PostgreSQL
work and connection occupancy on the Gmail notification path.
This is a targeted database optimization. It should reduce pressure
generated by this high-frequency query, but it is not expected to
resolve every source of API tail latency by itself.
## Safety and rollout
- The instance command uses `CREATE INDEX IF NOT EXISTS` and `DROP INDEX
IF EXISTS`.
- No uniqueness or application behavior changes are introduced.
- Existing rows require no data backfill.
- The index adds bounded storage and write-maintenance overhead.
## Validation
- `yarn nx typecheck twenty-server`
- Type-aware Oxlint on the changed files
- Oxfmt on the changed files
- `git diff --check`
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23580?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. -->
|
||
|
|
fc6a95a37f |
Throttle local cache expiration sweeps (#23579)
## Context The workspace cache and core entity cache keep bounded in-process maps. Entries that have not been read for 30 minutes are removed by an expiration sweep. Before this PR, every cache read synchronously walked the entire local cache, including every stored version, before performing the actual lookup. The cost therefore grew with the number of cached entries even when there was nothing to expire. Both caches are used on common server request paths, so these repeated full-map scans add unnecessary CPU work and short-lived allocations, which can contribute to event-loop and garbage-collection pressure under load. ## What changed - Run each cache's expiration sweep at most once per minute. - Keep the existing expiration logic unchanged and use one captured timestamp for the complete sweep. - Cover both cache services with tests proving that repeated reads within the interval trigger one sweep and that sweeping resumes after the interval. Normal cache reads now pay only for a timestamp check and branch. The full `O(cache size)` scan runs at most once per minute per process. ## Safety This does not change cache freshness: - The 100 ms local freshness window and Redis hash validation still run as before. - Explicit cache invalidation is unchanged. - The 30-minute inactivity threshold is unchanged. - LRU eviction still runs when entries are inserted. - Existing local-cache size limits remain unchanged. An unused entry can remain in memory for at most one additional minute before the next sweep. This may marginally increase average retained memory, but it cannot cause unbounded growth or allow stale data to bypass the existing hash validation. ## Expected impact This removes a cache-size-dependent operation from a high-frequency path. The expected benefit is lower CPU and allocation overhead, less garbage-collection pressure, and improved tail latency when local caches are populated. This is intentionally a narrow optimization. It does not claim to address every source of API tail latency. ## Validation - `yarn nx typecheck twenty-server` - Type-aware Oxlint on the changed files - Oxfmt on the changed files - Targeted workspace-cache and core-entity-cache Jest suites, 23 tests passing |
||
|
|
65155fe50c |
feat(apps): add enqueueJob to run a logic function on the workers (#23527)
Closes twentyhq/core-team-issues#2742 A logic function run is capped by its own `timeoutSeconds` (900s max), so anything that can't finish in one run — a full re-sync, a per-record fan-out, a rate-limited third-party API — had no way to continue. This adds a way to hand that work to the workers. ## What it looks like for an app author ```ts import { enqueueJob } from 'twenty-sdk/logic-function'; await enqueueJob({ logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33', payload: { cursor: nextCursor }, retryLimit: 3, priority: 2, delayMs: 60_000, }); ``` The target runs in its own process with its own timeout budget. The classic shape is a function that enqueues *itself* with the next cursor until there is nothing left. ## Changes **twenty-shared** — `EnqueueJobInput` / `EnqueueJobOptions` / `EnqueueJobResult` in `application`. **twenty-server** — new `application-job` module under `core-modules/application`, following the `application-key-value` pattern: - `enqueueJob` mutation on the metadata API, `@AuthApplication`-scoped - the lookup is scoped to `applicationId` + `workspaceId` — that's the authorization boundary, an app can only enqueue its own logic functions, anything else is `LOGIC_FUNCTION_NOT_FOUND` - pushes a `LogicFunctionTriggerJob` onto the existing `logicFunctionQueue`, so the enqueued run goes through the same executor (and the same execution throttling) as every other trigger - the queued run inherits the caller's `userId`/`userWorkspaceId`, so its app access token carries the same permissions as the function that queued it **Job options** are range-checked via `ResolverValidationPipe`, since the values come from application code and an unbounded delay or retry count would let an app pin work in the shared queue: | Option | Default | Range | |--------|---------|-------| | `retryLimit` | `0` | `0`–`10` | | `priority` | queue default | `1`–`10` (lower first) | | `delayMs` | `0` | `0`–7 days | `retryLimit` defaults to `0` rather than inheriting the server-route path's `3`: retries re-run the whole handler, so opting in should be the author's explicit choice. **twenty-sdk** — `enqueueJob` in `twenty-sdk/logic-function`, same shape as `runAgent`/`kv`. **Docs** — new "Background Jobs" page under Extend → Apps → Logic, plus nav and overview entries. **Generated** — regenerated `twenty-front/src/generated-metadata` and `twenty-client-sdk/src/metadata/generated` for the new mutation. ## Tests - `application-job.service.spec.ts` — 5 unit tests: job options mapping, defaults, acting-user propagation, application-scoped lookup, not-found - `enqueue-job.integration-spec.ts` — 5 integration tests: rejects a non-`APPLICATION_ACCESS` token, enqueues a function the app owns, rejects a function owned by another application, rejects an unknown identifier, rejects out-of-range options All green locally, along with `typecheck` for `twenty-server`/`twenty-sdk` and oxlint/oxfmt on the touched files. ## Notes for review - The target is addressed by `universalIdentifier`, matching `runAgent({ agentUniversalIdentifier })` and `ServerRouteDispatchResult.targetLogicFunctionUniversalIdentifier`. Addressing by `name` would be friendlier, but logic function names aren't validated for uniqueness within an app — happy to add it as a convenience if you'd rather. - `enqueueJob` returns as soon as the job is accepted; it can't return the target's result, since the queue driver's `add` returns void. Documented, with a pointer to the KV store for handing results back. --- _Generated by [Claude Code](https://claude.ai/code/session_01QrYvGonS3HMdeuMAVjs5hR)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23527?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> |
||
|
|
6447b7f935 |
feat(workflow): make the flow atom authoritative for version content, read core behind a flag (#23499)
## What
The frontend half of the workflow-version read switch, plus the small
server query it consumes. Two ideas:
1. **One hook owns where content comes from.**
`useWorkflowVersionContent(workflowVersionId)` returns `{ trigger, steps
}` from the workspace record when `IS_WORKFLOW_VERSION_IN_CORE_ENABLED`
is off (default), and from the new `workflowVersionContent` core query
when on. Switching the source later (core-only, after the column drop)
is a change inside this one hook.
2. **`flowComponentState` becomes authoritative for the builder.** The
canvas, diagram and step output schemas derive from the jotai atom; the
atom is seeded once per version through the hook above; mutations keep
it up to date.
## Why the seeding change is required
Today `WorkflowDiagramEffect` re-seeds the atom from the Apollo record
on **every** `currentVersion` identity change. That has two
consequences:
- Three of the five step/edge hooks (`delete step`, `create edge`,
`delete edge`) never write the atom themselves; they only write the
record and the re-seed papers over it.
- The model breaks the moment content comes from a source mutations do
not write (i.e. core): the stale fetch would be re-applied over every
optimistic edit, and your just-added step would vanish from the canvas.
So the atom is now seeded **once per version**, and
`useUpdateWorkflowVersionCache` applies the mutation's
`stepsDiff`/`triggerDiff` to the atom directly. All five step/edge hooks
get that through their existing call, which closes the three-hook gap in
one move. The step-update, trigger and tidy-up hooks write the atom too.
The record-cache writes are all kept while `trigger`/`steps` still live
on the record (dropped later with the columns).
## The dead wire, now the refresh path
`shouldWorkflowRefetchRequestFamilyState` was set by
`WorkflowSSESubscribeEffect` (reconnect, other-tab create) and
**consumed by nothing**. It is now the external-refresh path: when set,
the builder refetches content and reseeds. Known trade-off: while
connected, another tab's edits no longer live-patch the canvas through
record cache updates (they arrive on reconnect, version switch or
reload). Given concurrent editing of one draft has no conflict handling
anyway, that seemed acceptable; easy to extend the SSE effect to set the
flag on update events if we want live propagation back.
## Untouched by design
- **Run visualizer**: feeds the same atom from the immutable
`workflowRun.state.flow` snapshot; that duality (version content or run
snapshot) is exactly why the atom stays separate from the record store.
- **Version visualizer** (read-only): reseeds on content change, safe
because nothing writes its instance optimistically.
- Peripheral readers of `currentVersion.trigger/steps` (test-workflow
command, headless command enrichment, if-else body, etc.) still read the
record. Correct while dual-writing continues; they move to the content
hook before workspace content writes stop (tracked in the migration
plan).
## Verification
- `nx typecheck` green on both packages; `oxfmt` + `oxlint --type-aware`
green on all 16 changed files
- Front unit tests: 134 suites / 993 tests green (the two hook tests
gained the visualizer instance context their hooks now require)
- New server integration test for `workflowVersionContent`
- **Live click-through pending**: step create/delete/duplicate, edge
create/delete, trigger edit, tidy-up, draft create/discard, activation,
version viewer, run viewer, with the flag off and on. The failure mode
this PR guards against (an edit vanishing from the canvas) does not show
up in typecheck or unit tests.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23499?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. -->
|
||
|
|
a3beea893d |
Revert the external link confirmation popup for front components (#23567)
Reverts #23270 and #23404. Links in front components navigate natively again, with no confirmation popup and no per-app trusted-origins state in localStorage. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23567?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. --> |
||
|
|
e4e1d24731 |
Prevent overlapping workspace cleanup executions (#23522)
## Context The suspended-workspace cleanup is a long-running scheduled job. Under database or cache pressure, BullMQ can consider an execution stalled and start a replacement on another worker while the original execution is still running. Both executions can then enumerate the same suspended workspaces and run destructive cleanup concurrently. This amplifies the initial slowdown: 1. Multiple cleanup transactions target the same workspace data. 2. Transactions wait on each other's locks. 3. Database connections remain occupied while waiting. 4. Other workers and API requests have fewer connections available. There is a second source of unnecessary lock duration in workspace deletion. The deletion transaction currently starts before field metadata is read from the workspace cache. If that lookup is slow, the transaction stays open during an unrelated cache wait. ## What changed ### Prevent overlapping scheduled cleanups - Acquire a non-blocking PostgreSQL advisory lock before listing suspended workspaces. - Skip the execution when another worker already holds the lock. - Keep the lock on one dedicated PostgreSQL session for the full callback. - Release the lock in all normal and error paths. - Discard the database connection if lock acquisition or release has an ambiguous failure, preventing a session that may still own the lock from returning to the pool. - Encapsulate this lifecycle in `PostgresAdvisoryLockService`, exported by `TypeORMModule`, so other coarse-grained jobs can reuse it without handling acquisition and release themselves. ### Shorten the workspace deletion transaction - Read field metadata and build deletion chunks before starting the transaction. - Pass the precomputed chunks into the transactional deletion loop. - Keep the existing deletion order and SQL behavior unchanged. ## Why a PostgreSQL advisory lock The lock needs to coordinate workers running in different pods. A PostgreSQL session advisory lock provides the required behavior: - It is shared across all workers using the same database. - Acquisition is non-blocking, a duplicate execution can exit immediately. - It has no TTL or renewal heartbeat that could expire during the same event-loop stall that caused BullMQ to recover the job. - PostgreSQL automatically releases it when the owning session or process disappears. This is deliberately scoped to `CleanSuspendedWorkspacesJob`. It prevents overlapping scheduled executions, but it is not an exactly-once mechanism or a global mutex around every workspace-deletion entry point. ## Expected impact - Prevent one slow cleanup execution from becoming several concurrent cleanup executions. - Reduce database lock contention and connection-pool pressure during cleanup. - Avoid holding deletion transaction locks while waiting for workspace-cache data. - Reduce cleanup-related API latency bursts without changing normal cleanup semantics. The advisory lock holds one core database connection for the duration of the scheduled cleanup. This is intentional and bounded to the single lock owner. ## Validation - Focused advisory-lock tests cover successful execution, contention, callback failure, and unsafe connection disposal when unlock fails. - Cleanup-job tests cover both the lock-owner and skipped-execution paths. - Workspace-service coverage verifies that field metadata is loaded before the deletion transaction starts. - `yarn nx typecheck twenty-server` - Oxlint, Prettier, and Oxfmt checks on the changed files |
||
|
|
dbd2eac69c |
Let the instance upgrade version reach releases without instance commands (#23552)
Fixes the CI failure on #23520: An upgrade version sequence has to at least contain one instance or one workspace command Workspaces commands do not run for the instance level and aren't triggered automatically Explaining this PR need <img width="1396" height="954" alt="image" src="https://github.com/user-attachments/assets/5455d05b-b286-482a-8914-808f55f3b0bf" /> ``` Upload failed: App requires Twenty server >=2.26.0 but this server is 2.25.0. ``` The server really is 2.26 (`TWENTY_CURRENT_VERSION = '2.26.0'`), but `validateServerCompatibility` resolves the instance version through `UpgradeMigrationService.getInferredVersion()`, which reads the last row in `core.upgradeMigration` with `workspaceId IS NULL AND isInitial = false` and takes the version prefix off its name. Instance commands are the only ones that write a `workspaceId`-null row, and `2-26/` ships none (only three workspace commands), so the highest instance command in the tree is still `2-25-instance-command-fast-1785230296000-add-is-hidden-to-agent-message`. A fully migrated 2.26 server infers 2.25.0, and any app declaring `engines.twenty: ">=2.26.0"` is unpublishable. Two defects, both of which `getWorkspaceCompletedVersion` already avoids: - **Not sequence-aware.** The workspace path walks the registered sequence and only credits a version once the cursor sits on that version's last step. The instance path just reads the cursor's prefix, so a version contributing zero instance commands is unreachable. - **Not status-aware.** `getLastAttemptedInstanceCommand` filters on `attempt = MAX(attempt)` but not on status, so a *failed* 2.25 command still made the server report 2.25.0. ## What changed `UpgradeStatusService` gains `getInstanceCompletedVersion()`, the instance-scope mirror of `getWorkspaceCompletedVersion`. It walks the sequence filtered to instance steps, requires the cursor to sit on the last instance step of its version *and* be `completed`, then advances through any later supported version that declares no instance command at all. The version-skipping rule is the part that unblocks 2.26: a release with no instance-level work has nothing for the cursor to land on, so it is reached as soon as the last version that does have instance commands is done. A version whose instance command exists but has not run still holds the cursor back. - `validateServerCompatibility` calls the new method; `UpgradeMigrationService` is no longer a dependency of `ApplicationVersionValidationService`. - `getInstanceStatus` reports it as `inferredVersion`, so the upgrade gauge metric and `upgrade:status` CLI stop showing 2.25.0 on a 2.26 server. - `getInferredVersion` is deleted. Its one remaining caller passed a command name, which is just `extractVersionFromCommandName`. - Cursor resolution is extracted to `resolve-completed-version-from-cursor.util`, now shared by both scopes; the skip rule lives in `advance-through-versions-without-instance-commands.util`. The asymmetry between the two scopes is intentional and stays: instance commands record a row per workspace as well, so workspace cursors land on both command kinds and never had this gap. ## Testing - `npx nx typecheck twenty-server` clean, `npx nx lint:diff-with-main twenty-server` clean. - 294 unit tests pass across the upgrade and application modules, including 7 new ones for `getInstanceCompletedVersion`. Two pin the boundary: a trailing workspace-only version is reached, a trailing version whose instance command has not run is not. - The fixture in `upgrade-status.service.spec.ts` used `1.21.0`/`1.22.0`/`1.23.0`, which are real entries in `TWENTY_PREVIOUS_VERSIONS`. With the skip rule in place that sequence read as "every version from 2.0 onward has no instance commands" and walked to the end, so the fixture is renumbered to `0.2x.0` to keep those tests on cursor resolution alone. - `failing-app-installation-workspace-version.integration-spec.ts` already carried a comment describing this bug as a hazard it worked around. The workaround still holds, but integration tests were not run here (no DB in this session) — the stale comment is updated. #23520 stays at `>=2.26.0` and unblocks once this lands. --- _Generated by [Claude Code](https://claude.ai/code/session_012SvBG1BB3jTaZs6LA2Wi9R)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23552?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. --> |
||
|
|
d5d0726216 |
i18n - translations (#23563)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23563?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> |
||
|
|
38ad13655c |
Auto-start the workspace setup chat with a data model proposal (#23437)
https://github.com/user-attachments/assets/d447372b-c1b4-4c95-bac9-a8f8efa7d4a1 When the workspace creator lands on `/workspace-setup` after onboarding, the AI chat now starts on its own: an invisible first message, built server-side from the company enrichment collected in #23199, asks the assistant to propose a data model tailored to the business. The proposal streams in; the user never sees the prompt. - New `startWorkspaceSetupChat` mutation: creator only, gated on `IS_ONBOARDING_AI_CHAT_ENABLED`, available models and credits. Idempotent per user and workspace via a `keyValuePair` pointing at the thread, so a reload or a second tab joins the same conversation instead of starting a new one. - The thread holds exactly one hidden `USER` message combining the company context and the setup instructions, which keeps the one-hidden-message-per-thread index from #23199 satisfied. It goes through a dedicated streaming path that never queues, so the prompt cannot resurface as a visible message. - The assistant only proposes. It creates nothing until the user approves, then builds the model with the `metadata-building` skill. Objects and fields get English names with labels in the user's language, and the conversation continues in that language. - With no enrichment (consumer email domain, or the integration disabled) the kickoff still runs, and the assistant asks one short question about the business before proposing. - `findLatestSentUserMessage` no longer filters out hidden messages, so a failed kickoff turn stays retryable, and the no-message chat error surface now offers retry for stream errors. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23437?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. --> |
||
|
|
079e9b8e56 |
feat(dashboard): extend number format option to bar, line and pie charts (#23505)
https://discord.com/channels/1130383047699738754/1509604545381142649 Extends the Format option (Short/Full) added for the Number widget in #21521 to bar, line and pie charts. Format controls the numbers printed on the chart face: data labels and the pie center metric. Axis ticks stay abbreviated and tooltips always show the full value. Defaults to Short, so existing charts render unchanged. Server: nullable `numberFormat` on the bar/line/pie configuration DTOs, exposed in the dashboard AI tool schema. No migration, configuration is jsonb. Deferred: - The Format row has no visible effect while data labels are off, since tooltips are always full. - Number widget format defaults differ by field type (CURRENCY defaults to Short, NUMBER to Full). Pre-existing, untouched here. https://github.com/user-attachments/assets/0778f08a-6681-4e7a-8716-fb3026d1e01f <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23505?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. --> |
||
|
|
0b335d15b3 |
Refresh billing state after ending trial period (#23534)
Fixes #23530 After adding a credit card in the billing prompt, the credits section and subscription details stayed stale until a full page refresh. The `endSubscriptionTrialPeriod` mutation only returned `status` and `hasPaymentMethod`, and the frontend hook only patched the subscription status into the workspace state. The credits query was never refetched, so granted credits kept showing trial values, and `currentPeriodEnd` (renewal date) and `billingCustomer.hasPaymentMethod` stayed outdated. The backend already syncs everything to the database synchronously before the mutation returns, so fresh data was available, just never fetched. Changes: - `BillingEndTrialPeriodDTO` now includes nullable `currentBillingSubscription` and `billingSubscriptions`, returned by the resolver on success, mirroring the other billing update mutations (`switchSubscriptionInterval`, etc.) - `useEndSubscriptionTrialPeriod` applies the full billing update via `useApplyCurrentWorkspaceBillingUpdate` (falling back to the previous status-only patch), marks the billing customer as having a payment method, and refetches `GetResourceCreditUsage` so the credits section updates for any active observer This covers all entry points that end the trial: the billing page card modal, the trial banner, the AI chat banner, and the return from the Stripe portal. --- _Generated by [Claude Code](https://claude.ai/code/session_01W1J7cW2MhaqXGfdjvHeFoX)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23534?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. --> |
||
|
|
8b707c5131 |
i18n - translations (#23547)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23547?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> |
||
|
|
4ff9cba76d |
fix(server): stop the global catch-all filter from shadowing typed GraphQL exception filters (#23508)
## Context Sentry [TWENTY-SERVER-60Y](https://twenty-v7.sentry.io/issues/6633503406) ("Permission Denied: Entity performing the request does not have permission") is still firing at full rate on `v2.25.0`: ~10.8k events in the last 7 days, 24k total. #23104 tried to fix it by registering `PermissionsGraphqlApiExceptionFilter` globally via `APP_FILTER`. That registration is correct but **inert in production**, and the integration test added alongside it passes for a reason unrelated to prod behaviour. ## Root cause `main.ts` registered a catch-all filter after bootstrap: ```ts app.useGlobalFilters(new UnhandledExceptionFilter()); ``` Nest builds each resolver's filter list as `[...global, ...class, ...method]`, reverses it, and selects **exactly one** matching filter — there is no chaining. `APP_FILTER` providers are collected during module scan; `useGlobalFilters` appends after that, so the catch-all ended up at the head of the list: ``` 1. UnhandledExceptionFilter @Catch() <- matches everything, wins 2. PermissionsGraphqlApiExceptionFilter <- never reached 3. BillingGraphqlApiExceptionFilter <- never reached ``` On a GraphQL host `UnhandledExceptionFilter` then no-ops: `host.switchToHttp().getResponse()` returns the GraphQL args object, `response.header` is undefined, so it hits `return;`. Nest treats a falsy return as unhandled and rethrows the original `PermissionsException`, which reaches the Yoga error hook as a non-`BaseGraphQLError`, is serialized `INTERNAL_SERVER_ERROR`, and is reported by `shouldCaptureException`. The 28 resolvers carrying `@UseFilters(PermissionsGraphqlApiExceptionFilter)` were unaffected — method-level filters are evaluated before globals. Only the resolvers relying on the global registration leaked, which is exactly the set showing up in Sentry (`findOneApplication`, `uploadFilesFieldFileByUniversalIdentifier`, `UpdatePageLayoutWithTabsAndWidgets`, ...). Two other global filters were shadowed the same way and have never run: `BillingGraphqlApiExceptionFilter` and `FlatEntityMapsGraphqlApiExceptionFilter`. ## Why the existing test did not catch it `test/integration/utils/create-app.ts` builds the app from `AppModule` directly and never executes `main.ts`, so `useGlobalFilters` does not exist in the test process. It registered `MockedUnhandledExceptionFilter` as an `APP_FILTER` on the root testing module, which is collected *first* and therefore evaluated *last* — the exact inverse of production precedence. The `findOneApplication` denial test passed while the same query kept reporting to Sentry. ## Fix Register `UnhandledExceptionFilter` through `APP_FILTER` on `AppModule`. Root-module providers are scanned first, so it is collected first and evaluated last. The filter stays global, stays catch-all, and keeps its CORS-header role for HTTP; it simply no longer cuts in front of the typed filters. Un-shadowing the other two global filters means they now actually run, so `FileStorageExceptionFilter` and `FlatEntityMapsGraphqlApiExceptionFilter` get the `host.getType() !== 'graphql'` rethrow that `Billing` and `Permissions` already had. Without it they would start throwing GraphQL error objects into the REST pipeline. `MockedUnhandledExceptionFilter` is removed: `AppModule` now supplies the real filter in the same position, so the mock was dead weight. ## Test Verified against a real server (not the integration harness), calling the exact document from Sentry event `8d19eb7c` as a member with no permission flags: ``` query ($v1:UUID){findOneApplication(id:$v1){applicationVariables{key,value}}} ``` | | response code | exceptions captured | |---|---|---| | before | `INTERNAL_SERVER_ERROR` | 1 | | after | `FORBIDDEN` | 0 | Capture count measured through the console exception-handler driver, i.e. the same `captureExceptions` call site that is the Sentry driver in production. New unit spec `src/filters/__tests__/unhandled-exception.filter.spec.ts` boots a Nest + Yoga app both ways: it asserts `FORBIDDEN` with the `APP_FILTER` registration, and pins the shadowing behaviour of `app.useGlobalFilters` so the pattern cannot come back unnoticed. `granular-settings-permissions.integration-spec.ts` passes (10/10). Note it also passes *without* this fix — the harness cannot observe bootstrap-only configuration, which is the underlying reason #23104 shipped green. Closing that gap properly means sharing the post-`create` bootstrap between `main.ts` and `create-app.ts`; left as a follow-up. `file-storage-exception-filter.spec.ts` extended with a non-GraphQL host case. ## CI follow-up `failing-file-by-id-download.integration-spec.ts` snapshots were updated. That REST endpoint's 403 body changed in tests from `{}` to `{"statusCode":403,"error":"Forbidden","message":"Forbidden resource"}`. The old `{}` was an artifact of the mock: `MockedUnhandledExceptionFilter` rethrew, the exception escaped Nest's handler into Express's default error handler, and supertest saw an empty body. Production has always run the real `UnhandledExceptionFilter`, which writes `response.status(status).json(exception.response)` — the new snapshot. Production HTTP behaviour is unchanged by this PR: no other global filter matches an `HttpException` (the typed ones rethrow outside GraphQL), so the same filter handles it whether it is evaluated first or last. |
||
|
|
a276f3277f |
feat(workflow): pin concrete model on AI agent node creation and exclude interactive tools from workflow runs (#23447)
## Context The AI Agent workflow node's model dropdown could show a model that was not the one used at run time (e.g. the node displayed "Claude Haiku 4.5" while the run log showed `openai/gpt-5.6-sol`). Root cause: workflow agents were created with `modelId: AUTO_SELECT_SMART_MODEL_ID`. The builder's model `Select` cannot represent that value — auto-select ids are filtered out of the options (`useWorkspaceAiModelAvailability`) and the pinned "default" option remaps its value to the resolved concrete model id (`useAiModelOptions`) — so `Select` silently fell back to `options[0]`, the alphabetically first enabled model. Meanwhile the runtime correctly resolved auto-select to the instance's default smart model. ## What this PR does ### 1. New workflow agents store a concrete model id `WorkflowVersionStepOperationsWorkspaceService` now reads the workspace's `fastModel` setting, expands it through `AiModelRegistryService.getEffectiveModelConfig`, validates it with `validateModelAvailability`, and stores the concrete model id — so the dropdown displays the model that will actually run, and workflow agents default to the cheaper fast tier instead of the smart one. Falls back to `AUTO_SELECT_FAST_MODEL_ID` if the lookup or validation fails (workspace missing, no AI provider configured, model disabled), so node creation never breaks. ### 2. Exclude `search_help_center` and `navigate_app` from workflow agent runs `ActionToolProvider` adds both tools unconditionally, but they only make sense in an interactive chat session (navigation targets the user's browser; help-center search is a support tool). They are now excluded via `WORKFLOW_AGENT_EXCLUDED_TOOL_NAMES` in `AgentAsyncExecutorService`, alongside the existing output-navigation exclusions. Chat agents are unaffected. ## Test coverage - Existing specs for `WorkflowVersionStepOperationsWorkspaceService` and `AgentAsyncExecutorService` updated/passing (new constructor deps mocked). - `nx typecheck twenty-server` and `lint:diff-with-main` pass. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23447?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. --> |
||
|
|
c4a79c50c3 |
Install pre-installed apps in a dedicated job after the workspace upgrade cursor is written (#23517)
## Problem Application registrations flagged `isPreInstalled: true` were not installed on newly created workspaces. The call was wired in, but it ran too early. `activateWorkspace` invoked `preInstalledAppsService.installOnWorkspace` from inside `prefillCreatedWorkspaceRecords`, which runs **before** `activateAndInitializeUpgradeState`. The install path validates app/workspace version compatibility: - `ApplicationInstallService.runInstall` reads `engines.twenty` from the app's `package.json` and calls `validateWorkspaceCompatibility` - `ApplicationVersionValidationService.validateWorkspaceCompatibility` resolves the workspace version through `UpgradeStatusService.getWorkspaceCompletedVersion` - that reads the workspace's upgrade-migration cursor, which is only written by `markAsWorkspaceInitial` inside `activateAndInitializeUpgradeState` During creation the workspace has no cursor row yet, so `getWorkspaceCompletedVersion` returns `null`, the install throws `INVALID_WORKSPACE_VERSION`, and the failure is swallowed twice over: `PreInstalledAppsService` logs per-app failures without rethrowing, and `activateWorkspace` wraps the whole call in non-critical error handling. The workspace comes up silently missing its apps. This affects most real apps, since they pin `engines.twenty`: `fireflies`, `last-contact`, `people-data-labs`, `call-recorder`, `postcard`, `self-hosting`, `twenty-partners` (`>=2.23.0`) and `exa`, `real-estate` (`>=2.19.0`). Only apps with no `engines.twenty` installed successfully. The same interaction is already documented in `2-23-workspace-command-1784565137000-upgrade-people-data-labs-application.command.ts`, which works around it with `skipWorkspaceCompatibilityCheck: true`. ## Changes - Added `InstallPreInstalledAppsJob` on the workspace queue, mirroring the existing `InstallOnboardingAppsJob`. - `activateWorkspace` now enqueues that job instead of installing synchronously, so workspace creation no longer blocks on package fetching and manifest application. - The enqueue happens after `activateAndInitializeUpgradeState` writes the upgrade cursor, so the compatibility check has a workspace version to resolve by the time the worker picks the job up. ## Notes Workspaces created before this fix can be repaired with the existing `install-pre-installed-apps` backfill command, which is idempotent. |
||
|
|
e5c6cbcf80 |
Resolve route-trigger workspace from bearer token on bare hosts (#23490)
When a request reaches the `/s` route on a host that names no workspace
(bare `SERVER_URL` on a multiworkspace instance), resolve the workspace
from the bearer token — the same source `/graphql` uses — instead of
failing with `WORKSPACE_NOT_FOUND`. Hosts that do name a workspace keep
host resolution unchanged, and requests without a token are unaffected.
This makes the client SDK's same-site `${apiBase}/s` fallback work on
multiworkspace instances without a configured public domain: app logic
functions calling their own HTTP routes (e.g. call-recorder artifact
import) currently 404 there, because `TWENTY_FUNCTIONS_URL` is injected
empty and the bare server host carries no workspace identity. Cloud
(workspace public origin injected) and single-workspace self-host (host
resolves the default workspace) never hit this path.
Note: this also allows public routes to be reached through a bare host
when a valid token identifies the workspace. It does not change route
authorization; the token is used only for workspace resolution.
|
||
|
|
cedb3768ec |
i18n - translations (#23513)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23513?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> |
||
|
|
133b3375b6 |
[Upgrade] Stop command gracefully on SIGINT/SIGTERM (#23481)
Ctrl+C on `upgrade` used to kill the process wherever it happened to be, potentially in the middle of a workspace command. It now stops at the next iteration boundary instead. ## Behavior - **First SIGINT/SIGTERM** — the runner finishes what it started, then stops instead of starting new work. Exits with `130` (SIGINT) or `143` (SIGTERM), following the 128+signal convention, so orchestrators can tell an interruption apart from a failure. - **Second signal** — immediate exit, leaving the command in progress unfinished. - **SIGKILL** — untrappable, same outcome as a second signal. Nothing is rolled back on stop: the run resumes from the last command recorded in `upgradeMigration`. ## Opt-in per command Registering a `SIGINT` listener removes Node's default kill-on-signal behavior, so a command that installs a handler without honoring the flag would ignore the first Ctrl+C entirely. Handlers are therefore opt-in via `CommandShutdownService.listenToShutdownSignals()`, called by the two commands that stop at a boundary: - `UpgradeCommand` - `WorkspaceCommandRunner`, the base for standalone workspace commands Everything else keeps today's behavior and dies on the first signal, `run-instance-commands` included: instance commands are transactional and cursor-guarded, so a hard kill rolls back and a rerun skips what completed. `install-application`, `rebuild-application-default-deps` and `install-pre-installed-apps` iterate over workspaces without going through `WorkspaceCommandRunner`, so they are not armed either; they are one call away if we want them. The server and worker processes share these services and never arm anything, so their shutdown semantics are unchanged. ## Where the flag is checked `CommandShutdownService` exposes a single boolean, `isShutdownRequested()`, read only by the iteration runners: - `UpgradeSequenceRunnerService.runInner` — before each sequence step - `WorkspaceIteratorService.iterate` — before each workspace There is deliberately no `AbortSignal`: in-flight work is never cancelled, it is allowed to finish. Individual commands know nothing about shutdown, so a workspace that has started runs its whole pending segment before the run stops. Each workspace ends up either fully done with the segment or untouched, never scattered at some cursor inside it. That keeps resume state coarse and the change out of the command layer, at the cost of a longer stop latency, which the second Ctrl+C covers. `WorkspaceIteratorReport` gained an `interrupted` flag. The sequence runner needs it: stopping partway through the workspace list and then advancing the cursor would run an instance step against workspaces that are not aligned yet, so it returns instead. ## Deployment note Under Kubernetes, `terminationGracePeriodSeconds` must exceed the time for one workspace to finish its segment, otherwise the SIGTERM path degrades into a SIGKILL. Documented in `docs/UPGRADE_COMMANDS.md`. ## Testing - New unit test for `CommandShutdownService` (7 cases); 293 tests pass across `database/commands` and `core-modules/upgrade` - `tsgo -p tsconfig.json` clean - oxlint and oxfmt clean on all touched files |
||
|
|
742f76e318 |
[BREAKING-CHANGE] Add NOT_RECORDED call recording status (#23478)
Adds NOT_RECORDED to the CallRecording status select, for meetings where nothing was captured (bot never admitted, meeting not started, nobody joined). First part of twentyhq/core-team-issues#2706, split out so existing workspaces are upgraded before the call-recorder app starts writing the new status. - NOT_RECORDED enum value + standard select option - 2-26 workspace upgrade command adding the option to existing workspaces (idempotent, same option id as the standard definition) App-side classification from Recall sub codes comes in a follow-up PR. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23478?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. --> |
||
|
|
4ec65ed08d |
System view tooling explicit params key naming (#23506)
# Introduction View field system always result from a field existence, the application universal identifier should be the related field one Same but for views and object <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23506?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. --> |
||
|
|
e79424ddb0 |
Stop building the Campaigns navigation menu item (#23493)
Campaigns show up in the sidebar of every workspace, including brand new ones, while the feature is still behind `IS_EMAIL_GROUP_ENABLED`. ## Why it leaked `navigationMenuItem` has no `conditionalAvailabilityExpression` column. Only two entities do: - `page-layout-widget.entity.ts` - `command-menu-item.entity.ts` So there was no flag to attach. #23188 gated every surface that supports gating — the campaign command menu items carry `featureFlags.IS_EMAIL_GROUP_ENABLED` (`standard-command-menu-item.constant.ts:720` and `:735`) and the page layout widgets are gated the same way — but `allMessageCampaigns` was added to `FLAT_NAVIGATION_MENU_ITEM_NAMES`, which builds unconditionally. `messageCampaign` is `isSystem: true`, so the navigation item was the only thing exposing the feature. ## Change - Drop `allMessageCampaigns` from `FLAT_NAVIGATION_MENU_ITEM_NAMES`. Its definition stays in `STANDARD_NAVIGATION_MENU_ITEMS` so the identifier remains reserved and re-enabling is a one-line change. - Add workspace command `1785324390000` to delete the rows from workspaces already provisioned with the item. It collects every matching row rather than looking the identifier up once, because each user workspace gets its own. ## Trade-off Campaigns become unreachable from the sidebar even with the flag on, which restores the pre-#23188 state. The durable fix is adding `conditionalAvailabilityExpression` to `navigationMenuItem` so the item can be gated like the command menu items — a schema change, deliberately not done here. ## Verification Reproduced on a fresh `database:reset` against `main`: two `20202020-b00b-4b0b-8b0b-c0aba11c000b` rows (type `OBJECT`, position 7) for the single seeded workspace, structurally identical to the other nav items. Typecheck, `oxfmt --check src/` and `oxlint --type-aware src/` clean on this branch. The post-fix database check did not complete — local Postgres died mid-reset — so the removal is verified by construction and by the build-list change, not yet by a second reset. --- _Generated by [Claude Code](https://claude.ai/code/session_01NZKeMSUBG3VQPuoy8i5Awe)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23493?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. --> |
||
|
|
e99452e00d |
feat(server): attach app attributes to application rate-limited metric (#23500)
## What
`MetricsKeys.CommonApiApplicationQueryRateLimited`
(`common-api-query/application-rate-limited`) is emitted from the
per-application throttler in `common-base-query-runner.service.ts`
**without any attributes**. Downstream that means a single
undifferentiated counter — there is no way to tell *which* application
is hitting `APPLICATION_API_RATE_LIMITING_LIMIT`.
This attaches the app dimension:
```ts
await this.metricsService.incrementCounterForEvent({
key: MetricsKeys.CommonApiApplicationQueryRateLimited,
shouldStoreInCache: false,
attributes: {
universal_identifier: authContext.application.universalIdentifier,
app_name: authContext.application.name,
source_type: authContext.application.sourceType,
},
});
```
## Why these three attributes
Same trio already emitted by `application-registration.service.ts`,
`application-install.service.ts` and `application-gauge.service.ts`, so
per-app API rate limiting joins cleanly against the existing app
lifecycle metrics rather than introducing a second naming scheme.
## Notes
- **No new data fetching.** `authContext` is already narrowed to
`ApplicationWorkspaceAuthContext` by the `isApplicationAuthContext`
guard at the top of the method, and the throttler key a few lines above
already reads `authContext.application.universalIdentifier`. `name` and
`sourceType` are plain columns on `FlatApplication`, present on the same
in-memory object.
- **Bounded cardinality.** `universalIdentifier` is stable for an
application across workspaces (see the unique index on
`(universalIdentifier, workspaceId)`), so the label set is bounded by
the number of distinct applications, not by installs.
- **`source_type` typing.** `ApplicationRegistrationSourceType` is a
string enum, assignable to OTel's `AttributeValue`.
## Context
The consuming dashboard panels are already merged-pending in
`twentyhq/twenty-infra` ([PR
#835](https://github.com/twentyhq/twenty-infra/pull/835)) and written
with a `coalesce(nullIf(Attributes['app_name'], ''),
nullIf(Attributes['universal_identifier'], ''), 'unknown')` fallback, so
they render today as a single `unknown` series and start splitting per
app automatically once this ships — no dashboard change needed on either
side of the deploy.
## Test plan
- Behaviour is unchanged: the counter still fires once per
`ThrottlerException`, and the error is still rethrown. Only the
attribute bag is new.
- I did not run the twenty-server test suite or typecheck locally — this
was authored against a shallow clone without a monorepo install, so I'm
relying on CI for both.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VbFtaCS5RkxDhLJApNSteV)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23500?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. -->
|
||
|
|
6db019686e |
i18n - translations (#23507)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
0c545bcdeb |
[BREAKING-CHANGE] Centralize system View viewField side effect (#23081)
# Introduction Closes https://github.com/twentyhq/core-team-issues/issues/2669 Part of the `isSystemSideEffect` engine-ownership effort. Until now, a custom object's default **INDEX** table view (`All {objectLabelPlural}`) and its view fields were built imperatively in `ObjectMetadataService` with random `v4()` identifiers, while `twenty-standard` authored its own copies with hardcoded literals. The two never converged, an object rename could drift the view, and nothing marked these rows as engine-owned. This PR makes the metadata side-effect engine the **single owner** of the INDEX view and its view fields, on name-free deterministic identifiers, for custom and standard objects alike. ## Core design - **Name-free deterministic identity.** The INDEX view identifier derives from `object identifier + ViewKey.INDEX` (`getSystemViewUniversalIdentifier`); each view-field identifier derives from `view identifier + field identifier` (`getViewFieldUniversalIdentifier`). An object rename (with a pinned object identifier) keeps the same view, losslessly. - **`isSystemSideEffect: true` is provenance.** Every INDEX view / view field the engine emits is flagged system-owned, so manifest deletion inference never drops it. The flag follows the view: a view field inherits its parent view's flag. - **The engine is the sole owner of the INDEX view.** It always emits it; a caller providing one with the same derived identifier is a genuine conflict surfaced by the engine's reserved-identifier collision, not silently deferred. ## Changes ### Shared (`twenty-shared`) - `getIndexViewUniversalIdentifier` → `getSystemViewUniversalIdentifier`, now taking a `viewKey` (generalizes to any singleton engine-owned view). - Standard field identifiers extracted into a new `STANDARD_OBJECT_FIELDS` constant, so both an object's `fields` and its INDEX view read the same field identifiers. - `buildStandardObjectIndexView` derives the standard INDEX view + view-field identifiers from `STANDARD_OBJECT_FIELDS`, replacing the hardcoded literals in `standard-object.constant.ts`. ### Metadata side-effect engine (custom objects) - **`objectSystemFieldsAndIndexViewOnCreate`** (replaces `objectSystemFieldsOnCreate`): on object creation, provisions the 7 reserved system fields **and** the INDEX view with one view field per displayable system field, all `isSystemSideEffect: true`. - **`fieldIndexViewFieldOnCreate`** (new): on field creation, provisions the field's INDEX view field. Object created in the same batch → visible, positioned before the system view fields; pre-existing object → hidden, appended (preserving the historical `createOneField` behavior). Both branches resolve the INDEX view by its derived identifier (single map access, never a scan). - **`fieldSystemViewFieldsOnDelete`** (new): on field deletion, cascade-deletes every engine-owned view field displaying it. - **`objectSystemSideEffectsOnDelete`** (extended): now also cascade-deletes the object's engine-owned views and their view fields (in addition to system fields, indexes, searchFieldMetadata). Every lookup walks a foreign-key aggregator down from the deleted object, so the work is proportional to what the object owns, never to workspace size. - Object-create and field-create positions are derived from the same caller-input field list, so the INDEX view layout is contiguous with no handler-ordering dependency. - `view` / `viewField` added to the side-effect companion metadata names for `fieldMetadata` and `objectMetadata`. ### Reserved-identifier invariant A caller can never define an entity whose identifier collides with one a system side effect produces: caller inputs are forced `isSystemSideEffect: false` at every entry point (API and app-manifest transpilers), and the engine raises `RESERVED_SYSTEM_UNIVERSAL_IDENTIFIER`, aborting the operation, when a system emission lands on a caller-claimed identifier. Covered by a new engine-level test. ### Caller-side provisioning removed The imperative INDEX view + view-field provisioning is removed from `ObjectMetadataService.createOneObject`. The record-page `FIELDS_WIDGET` view is intentionally left caller-side and deferred to the follow-up (see below). ### `twenty-standard` convergence Standard INDEX views and their view fields converge on the same derived-identifier + `isSystemSideEffect: true` scheme as the engine. `twenty-standard` syncs through the from/to migration path (which never runs the side-effect engine), so it authors this INDEX surface itself, matching what the engine produces for custom objects. ## Rollout Two `2.26.0` workspace commands, running after the `2.25` messageCampaign commands: - `upgrade:2-26:reconcile-index-view-universal-identifier` re-owns the INDEX views of the **twenty-standard and workspace-custom applications** and all their view fields to the derived identifiers with `isSystemSideEffect: true`, in a single per-workspace transaction. Each view field identifier is keyed on the application of the **displayed field** (an app or user column on a standard INDEX view converges too). Soft-deleted views and view fields are skipped: one can coexist with an active successor on the same derivation inputs and both would derive the same identifier. Children reference the view by primary key, so the re-own is lossless. - `upgrade:2-26:demote-and-backfill-application-index-view` handles **manifest-installed applications**, which never had their INDEX view auto-provisioned: every caller-authored INDEX view of another application is demoted to `key: null` (a plain additional view under its manifest identifier), then every application object gets the engine-owned INDEX view and its full view-field layout backfilled through the migration pipeline's legacy path (no side-effect expansion), views committed before view fields across applications since a view field belongs to the application owning its field. Idempotent and retry-safe: engine-owned INDEX views are neither demoted nor re-backfilled, and view creation and view-field creation are gated independently, so a retry after a partial failure still backfills the missing view fields of an already-committed view. Both support `--dry-run` and invalidate the full flat-maps closure (parents aggregate the re-owned identifiers, children resolve them as universal foreign keys, and page-layout widget universal configurations resolve view PKs at cache-build time). The `2.25` `upgrade:2-25:add-message-campaign-name-field` command is adapted to resolve the campaign INDEX view by its INDEX key on the object instead of by universal identifier: it now runs before the reconcile, on workspaces still holding legacy identifiers. ## ⚠️ Breaking change This PR **mutates 187 previously hardcoded universal identifiers** — the standard objects' INDEX views and their view fields (the literals removed from `standard-object.constant.ts`), now derived. - **Handled by the `2.26` commands above** for all existing workspaces. - **The INDEX key is now engine-reserved.** The flat view validator rejects caller-created INDEX views (API and manifest inputs are forced `isSystemSideEffect: false`) and enforces a single non-deleted INDEX view per object; `view.key` is no longer a comparable/updatable property, so no writer can promote or demote a view after creation. `ViewManifest.key` is deprecated and ignored (manifest views are always additional views, so old apps keep syncing and demoted views are not promoted back); the REST/GraphQL create path now rejects `key: INDEX`. In-repo example apps (`hello-world`, `document-generator`) no longer declare it. - **12 declared-but-never-seeded standard INDEX view field identifiers deleted** (the former `preservedViewFields` on `timelineActivity`, `workflowRun` and `workspaceMember`): after the reconcile, no workspace row references them. - **`computeFlatViewFieldsToCreate` now derives view field identifiers** instead of drawing `v4()` ones, which also changes what the committed `1-23` record-page backfill produces going forward (deliberate, documented in-code). - **Record-page views and view fields are not affected** (identifiers unchanged). - **In-repo apps: `twenty-last-contact` updated.** It was the only app declaring explicit INDEX view fields (10 columns across `allPeople` / `allCompanies` / `allOpportunities`) through manifest `viewFields`. Those target identifiers are now engine-owned and derived, so the manifest inputs no longer resolve and install failed with `View not found`. The app now declares only its fields; the engine's `fieldIndexViewFieldOnCreate` provisions the matching INDEX view field automatically. No other app under `packages/twenty-apps` references any of the 187 mutated identifiers, and apps that target standard views point at record-page views (e.g. `real-estate` → `opportunityRecordPageFields`) or their own objects (`twenty-partners`), all unchanged. ### Loss of granularity for app maintainers The engine now owns the INDEX view field of every field a caller adds to an object, so app maintainers lose direct control over those columns. Previously an app could target the engine-owned INDEX view with an explicit manifest `viewField` and set its `position` and `isVisible`. Now `fieldIndexViewFieldOnCreate` appends a **hidden** view field in caller-input order on field creation, so: - Columns an app previously showed at a **dedicated position** and **visible** (e.g. `twenty-last-contact`'s last-contact columns) become **hidden** and **appended in input order** after install. - There is currently **no manifest way to override** the engine-provisioned INDEX view field's position, visibility, or size. This is a deliberate regression accepted for the sake of single-ownership, and app maintainers should expect their INDEX columns to move/hide after upgrading. A follow-up override API will let maintainers reclaim per-field control over the engine-provisioned INDEX view field. ## Testing - Unit specs for each handler: object create (system fields + INDEX view/view fields, override, position offset), field create (same-batch vs existing-object, non-displayable noop, no-INDEX-view noop), field delete, object delete (fields/indexes/searchFieldMetadata/views/view fields cascade, reverse-relation view field on another object). - Engine-level test for the reserved-identifier collision. - `twenty-standard` guard test that its INDEX views/view fields stay on the derived scheme and stay system-owned. - Integration test: full engine provisioning of the INDEX view/view fields on object creation, same view id preserved across an object rename, and cascade delete on object deletion. ## Follow-up The full record-page stack (record-page view, its view fields, view field groups, page layout / tab / widget) is still built imperatively and moves into the engine in https://github.com/twentyhq/core-team-issues/issues/2721. |
||
|
|
ea7863dc4e |
feat(ai-agent): lazy tool loading for the open-ended runAgent path (#23454)
## Context `AgentAsyncExecutorService.executeAgent` is shared by workflow agent nodes and the `runAgent` mutation (Slack assistant, apps). Workflow nodes run a scoped task, so pre-loading the few explicitly-granted object tools is fast and skips the `learn_tools` round trip (the behavior settled in #23400 / #23358). `runAgent` is open-ended: its role grants broad object access, so pre-loading inlines every CRUD/action schema on every step. That is what makes the Slack assistant take 3-4 minutes for a prompt Ask AI answers in seconds. Confirmed still slow with #23400 merged, so this is the payload, not object scoping. ## What Add a `toolLoadingStrategy` to `executeAgent` (default `'preload'`, so workflow nodes and evals are unchanged). `AgentRunService.run` opts into `'lazy'`, which exposes a compact tool catalog in the system prompt plus the `learn_tools` / `execute_tool` meta-tools, using composed role permissions rather than explicit grants only, so the agent keeps broad access without the full-payload latency. ## How - Split tool provisioning into two focused methods on the executor; a 3-line dispatch chooses per strategy. The pre-load path is unchanged. - `buildLazyRegistryToolset`: one reusable definition of lazy registry provisioning (catalog + meta-tools), so the chat and agent executors can share it. - Extract `buildToolCatalogSection` out of `SystemPromptBuilderService` into a `tool-provider` util so both paths format the catalog identically (no dup). - Replace the meta-tools' `excludeTools` denylist with a single `isToolAllowed` predicate: the agent path passes an allowlist closed over the shown catalog (enforced at call time), MCP passes its existing deny predicate. Workflow node and eval behavior is unchanged. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23454?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. --> |
||
|
|
5ebcce0a51 |
feat(ai-tool): resolve and default icons in AI metadata tools (#23480)
## Context Objects and fields created through the AI chat / MCP metadata tools almost never get an icon, so they all render with the meaningless `123` fallback icon. Two causes: - The `icon` tool input was described only as `"Icon name"`, so the model had no idea what the value space is and mostly skipped an optional field it couldn't fill confidently. - Any invalid name is silently swapped for `Icon123` by `useIcons.getIcon` on the frontend, so near-misses were indistinguishable from unset. ## What this PR does **Guide the model** (icon names are Tabler names, which LLMs know well): - `icon` / `targetFieldIcon` schema descriptions now state the convention with examples (`IconBuildingSkyscraper`, `IconPaw`, …) and ask for one to always be set - The `metadata-building` skill gains an "Icons" section; the MCP server instructions gain a one-line reminder **Normalize server-side** (new `resolveIconName` util, used by all create/update/batch metadata tool executes incl. `relationCreationPayload.targetFieldIcon`): - Fixes shape mistakes: raw tabler slugs (`"building-skyscraper"`), separators, missing or lowercased `Icon` prefix - Deliberately does NOT validate existence against the full ~4.2k icon registry — an unknown name is harmless since the frontend falls back to its default icon, exactly as for icons stored via the API today - Unusable input (empty/garbage) resolves to nothing: creates fall back to a default, updates keep the existing icon **Fall back sensibly for fields**: - New `FIELD_TYPE_DEFAULT_ICONS` in `twenty-shared/constants` maps every `FieldMetadataType` to a sensible icon (mirroring the settings UI type illustrations), applied when the model provides no usable icon — an AI-created field always gets a meaningful icon - Lives in twenty-shared so the frontend can reuse it later (e.g. as `getIcon`'s custom default for fields) The REST/GraphQL metadata APIs are untouched — this only affects the AI tool layer. ## Test plan - `resolve-icon-name.util.spec.ts` — canonical pass-through, slug/prefix/separator fixes, unknown-name pass-through (FE fallback contract), unusable inputs, icon-key dropping on updates - `FieldTypeDefaultIcons.test.ts` — every field type mapped, all values canonically shaped (values hand-checked against the twenty-ui `ALL_ICONS` registry) - `nx typecheck` twenty-server + twenty-shared, oxlint/oxfmt clean on changed files <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23480?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. --> |
||
|
|
a3b54e834c |
PDF upload fix (#23473)
Sometimes uploading PDF files resulted in "Non-whitespace before first tag." error, updating parsing library fixes the error <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23473?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. --> |
||
|
|
25d20731ac |
Include root cause errors in workspace migration runner exception message (#23416)
closes https://github.com/twentyhq/core-team-issues/issues/2733 ```diff - "message": "Migration action 'create' for 'index' (universalIdentifier: 67c6811e-...) failed" + "message": "Migration action 'create' for 'index' (universalIdentifier: 67c6811e-...) failed: [workspaceSchema] could not create unique index "IDX_UNIQUE_85951922a2..." (pg code: 23505, detail: Key ("externalId")=(DUPLICATED-VALUE) is duplicated.)" ``` ## Problem When a workspace migration action fails, the `EXECUTION_FAILED` exception message only states which action failed: ``` Migration action 'create' for 'index' (universalIdentifier: 9e20a0f6-7a18-51c5-a422-5dc4dbd1d972) failed ``` The underlying errors (`metadata`, `workspaceSchema`, `actionTranspilation`) are attached to the exception instance but dropped by most surfaces: the SDK CLI only prints `errors[0].message`, server logs only log `error.message`, and the REST/GraphQL paths lose the postgres driver details. Debugging a failed app install (like the stale index universalIdentifier case in the issue) requires guessing. ## Change - New `formatWorkspaceMigrationRunnerExecutionErrors` util that builds a compact one-line summary of the underlying execution errors, including the postgres error code and `detail` for `QueryFailedError`, capped at 1500 chars. - The `EXECUTION_FAILED` exception message now appends that summary: ``` Migration action 'create' for 'index' (universalIdentifier: 9e20a0f6-...) failed: [workspaceSchema] relation "IDX_..." already exists (pg code: 42P07) ``` Since every surface (CLI, server logs, Sentry, REST, GraphQL) shows `error.message`, the root cause now propagates everywhere without touching those surfaces. - Since actions run inside a single transaction, when one branch fails with `25P02 current transaction is aborted` (collateral of the other branch's statement aborting the transaction), the summary keeps only the real root cause. A lone 25P02 error is still shown. ## Notes - The SDK's `getSyncErrorRecoveryHint` matching (`/migration action .* failed/`) still works with the suffixed format. - Commit-time failures from `DEFERRABLE INITIALLY DEFERRED` FK constraints still bypass this path (wrapped as `INTERNAL_SERVER_ERROR` with no action attribution) and are left as a follow-up. ## Tests - New spec for the formatter util (labels, pg code/detail, 25P02 demotion, truncation). - Extended `workspace-migration-runner.exception.spec.ts` with a root-cause message assertion. - Updated `format-upgrade-error-for-storage` snapshots (first line now carries the enriched message). --- _Generated by [Claude Code](https://claude.ai/code/session_01Mu8t74X14oYVkLrFBZHs2o)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23416?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. --> |
||
|
|
198e3969df |
feat(workflow): read workflow version content from core in the engine, behind a flag (#23403)
## Scope: engine only Behind the existing `IS_WORKFLOW_VERSION_IN_CORE_ENABLED` flag (**off by default**), `getWorkflowVersionOrFail` sources a version's `trigger` / `steps` / `status` from `core.workflowVersion` instead of the workspace row. That covers its 11 call sites: workflow build, validation, schema, run, and trigger dispatch. **The UI is not switched here.** `useWorkflowVersion` and the content fetch in `useWorkflowWithCurrentVersion` still go through generic object CRUD against the workspace columns. That work needs the client to stop treating the workspace record as the home for content, and is planned separately around `flowComponentState` (the jotai state the builder already reads from). It is the read that must land before the workspace `trigger` / `steps` columns can be dropped. ## Why an overlay, not a repository swap `core.workflowVersion` is a content-only projection: its own `id` (not the workspace version id), `workflowId`, `triggers[]`, `steps[]`, `status`. No `name`, no `position`. So core cannot fully back the entity. The flip is therefore an overlay: identity, name and position stay from the workspace row, and only content comes from core (`triggers[0] -> trigger`). ## Reversibility Falls back to workspace content when the flag is off (default), when the version has no `coreWorkflowVersionId` soft-ref, or when the core row is missing. Merging changes nothing until the flag is enabled per workspace, and it can be flipped back at any time. ## Verification - `nx typecheck twenty-server` green, `oxfmt` + `oxlint --type-aware` green - Unit test covering four branches: flag off, flag on with the core row present (overlays content **and** preserves `id` / `name` from the workspace row), flag on with the core row missing (falls back on `trigger`, `steps` and `status`), flag on with no soft-ref (skips the core read) ## Enablement gate Do not enable the flag in any workspace until the drift dashboard reports zero drift for `core.workflowVersion` and legacy drift is repaired. Enabling before that turns latent drift into live dispatch behaviour. |
||
|
|
00e418a039 |
Show call recorders as calendar event participants (#23380)
Closes twentyhq/core-team-issues#2729 Call recordings attached to a calendar event are now displayed next to the human participants, in the timeline event card (`EventCardCalendarEvent`). They are rendered as the source app's `AppChip`, rounded so it sits in the participant avatar group, with the recording status in tooltip https://github.com/user-attachments/assets/e0c393c8-fd65-4468-8c4f-503dd22c13d5 ## Before No call recorder chip displayed TODO: add this in the calendar views (`CalendarEventRow`) |
||
|
|
965e033f3f |
[breaking-change] fix(server): return runAgent execution failures as result errors (#23390)
## Summary
- When `executeAgent` throws, `runAgent` now returns `{ success: false,
error }` instead of a GraphQL exception
- Callers (workflows, Slack assistant, etc.) can surface the failure to
users instead of hanging or failing opaquely
Run agent exception response error format breaking-change, errors moved
from the GraphQL error channel into the response payload.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23390?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. -->
|
||
|
|
4730542087 |
chore: bump version to 2.26.0 (#23451)
## 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/23451?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> |
||
|
|
f5da810e59 |
feat(server): add application:install command (#23430)
Adds `application:install` to install an application on workspaces that do not have it yet, and moves both application commands onto `WorkspaceIteratorService`. ### Behavior - Iterates provisioned workspaces through `WorkspaceIteratorService` (workspace id resolution, workspace context, per-workspace success/fail report), or the ones passed with `-w`. - Workspaces where the application is already installed are skipped, with a log line pointing at `application:upgrade`. This command never upgrades. - Installed workspaces are detected by `universalIdentifier`, the same identity `ApplicationInstallService` uses to tell a fresh install from a version upgrade, checked per workspace on the `(universalIdentifier, workspaceId)` unique index. - `--workspace-count-limit` caps both the iterator's own selection and an explicitly targeted `-w` list. - Fails fast for `LOCAL` and `OAUTH_ONLY` registrations, which have no code artifacts to install. - Per-workspace failures are collected in the iterator report and never abort the run; the command ends with an installed / skipped / failed summary. ### Options | Flag | Description | | --- | --- | | `-u, --application-registration-universal-identifier` | Application registration universal identifier (required) | | `-w, --workspace-id` | Target a specific workspace, repeatable | | `--workspace-count-limit` | Cap the number of workspaces to iterate over (max 50) | | `-d, --dry-run` | Print the workspaces that would be installed without installing | | `-y, --yes` | Skip the confirmation prompt | ### Example ``` yarn command:prod application:install -u UNIVERSAL_IDENTIFIER --dry-run ``` ### Changes to application:upgrade - `ApplicationUpgradeService.upgradeApplications` iterates through `WorkspaceIteratorService` and returns its report, replacing the hand-rolled parallel batching. - `--batch-size` dropped from the command, and `batchSize` dropped from the service and from `UpgradeApplicationsJobData`, since `iterate()` is sequential. - `parseBoundedPositiveInteger` moved to `src/database/commands/utils/` and is shared by both commands. ### Files - `application-install/commands/install-application.command.ts` (new) - `src/database/commands/utils/parse-bounded-positive-integer.util.ts` (new) - `application-upgrade/application-upgrade.service.ts`, `application-upgrade/commands/upgrade-application.command.ts`, `jobs/upgrade-applications.job*`: iterator instead of batching - `application-install.module.ts` / `application-upgrade.module.ts`: register the command, wire `WorkspaceIteratorModule` - `database-command.module.ts`: import `ApplicationInstallModule` so the command is discovered by the CLI ### Testing - `npx jest src/engine/core-modules/application` (32 suites, 181 tests passing) - `npx nx typecheck twenty-server` - `oxlint --type-aware` and `oxfmt` on the changed files |
||
|
|
942755d0dd |
fix(applications): display the installed application icon (#23411)
## Problem
After installing an app, its icon is missing across the UI, while
application *registration* icons render fine.
`Application.logo` holds the manifest path (`public/logo.svg`), which is
package-relative and not displayable. The server exposes a `logoUrl`
resolve field that turns it into
`/public-assets/{workspaceId}/{applicationId}/{logo}`, but on the front
end:
- `APPLICATION_FRAGMENT` and `FIND_MANY_APPLICATIONS` never selected
`Application.logoUrl`.
- So the only source of a usable logo url was
`currentWorkspace.installedApplications`, which is fetched by
`GetCurrentUser` at bootstrap. Nothing refreshed it after
`installApplication`, so a freshly installed app was absent from that
list.
- `useApplicationChipData` then fell through to
`fallbackApplicationData`, which callers populated with the raw `logo`
path. `getAbsoluteImageUrl('public/logo.svg')` yields
`{serverUrl}/public/logo.svg`, which 404s, so the avatar rendered as a
letter placeholder.
## Before / After
An app installed while the applications page is open, so the workspace
snapshot loaded at bootstrap does not know about it yet:
| Before | After |
|---|---|
| <img
src="https://raw.githubusercontent.com/twentyhq/twenty/e27a817fd95c5e44c1fbb64fd4fd68525daeee61/.pr-assets/app-icon-before.png"
width="480"> | <img
src="https://raw.githubusercontent.com/twentyhq/twenty/e27a817fd95c5e44c1fbb64fd4fd68525daeee61/.pr-assets/app-icon-after.png"
width="480"> |
## Changes
- Select `logoUrl` on `Application` in `APPLICATION_FRAGMENT` and
`FIND_MANY_APPLICATIONS`.
- Drop `logo` from `ApplicationDisplayData` and from the `AppChip` /
subtable fallback props, so a package-relative path can no longer reach
an `img` src. Call sites that already passed a url under `logo` now pass
`logoUrl`.
- `SettingsApplicationDetails` and `SettingsApplicationsTable` pass the
application's own `logoUrl`.
- On install, add the returned application to
`currentWorkspace.installedApplications` instead of reloading the
current user, so the chips that resolve by `applicationId` only (nav
menu items, object/field tables, tool rows, workflow nodes) pick it up.
- Stop exposing `logo` on the `Application` GraphQL type: nothing
selects it anymore, and having both `logo` (package-relative path) and
`logoUrl` (display url) was the source of the bug. The column is still
read server-side to build `logoUrl`.
- Regenerated `generated-metadata/graphql.ts`.
## Verification
Ran the stack locally against a seeded workspace with an installed app
whose logo lives at `public/logo.png`:
- `findManyApplications` returns a `logoUrl` under `/public-assets/...`,
and that url serves `200 image/png`.
- Reproduced the bug and the fix in the browser with the scenario shown
above (screenshots taken on the base commit and on this branch).
- `npx nx typecheck twenty-front`, `npx nx typecheck twenty-server`,
`npx nx lint:diff-with-main` on both, and the application settings jest
suites pass.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01N8r4z2dZ553nAnCNe7GxMH)_
[Review in
cubic](https://cubic.dev/pr/twentyhq/twenty/pull/23411?utm_source=github)
|
||
|
|
9509c737e0 |
Replace the onboarding AI chat feature flag with an environment variable (#23439)
Follow-up to #23199. The AI-chat onboarding is an instance-level rollout decision, not a per-workspace experiment, so `IS_ONBOARDING_AI_CHAT_ENABLED` becomes an instance config variable (default `false`, editable from the admin panel) exposed to the frontend through `ClientConfig`. The workspace feature flag is deleted; leftover `featureFlag` rows are inert since the column is plain text. `IS_WORKSPACE_COMPANY_ENRICHMENT_ENABLED` is removed as redundant: the PDL client already skips everything when no API key is set. Enrichment now runs when the AI chat is on and `PEOPLE_DATA_LABS_API_KEY` is configured. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23439?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. --> |
||
|
|
8e5969ea55 |
i18n - translations (#23432)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23432?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> |
||
|
|
f15fabb5d9 |
Enrich workspace company via People Data Labs during onboarding (#23199)
https://github.com/user-attachments/assets/fb9001c4-195d-4735-898b-07ccbab01677 During onboarding, the workspace creator's work-email domain is enriched through People Data Labs and stored client-side. The stacked workspace-setup PR folds it into the invisible prompt that kicks off the setup chat, so the assistant knows the company from its first reply. - New `enrichWorkspaceCompany` mutation: throttled, creator-only, work domains only. Off by default: requires the `IS_WORKSPACE_COMPANY_ENRICHMENT_ENABLED` instance config variable (default false), a `PEOPLE_DATA_LABS_API_KEY`, and the `IS_ONBOARDING_AI_CHAT_ENABLED` workspace feature flag (the enrichment only feeds the AI-chat workspace setup). Every attempt past the throttle is recorded per workspace in a `keyValuePair`. - The frontend fetches once during onboarding and stores a matched result in localStorage. This PR does not deliver it to the model: the hidden-message plumbing it adds (`isHidden` on `agentMessage`, excluded from the chat UI, thread ranking and the admin transcript, included in the model conversation) is what the stacked workspace-setup PR uses to send the context and the setup prompt as one invisible first message. - The PDL wire protocol (base URL, wire types, envelope parsing, error extraction) is kept as a small self-contained copy inside the server `company-enrichment` module. The standalone people-data-labs app keeps its own copy; the two are intentionally not shared, since the app and the core-engine usage are expected to evolve independently. - `WorkspaceCompanyEnrichment` lives in `twenty-shared/workspace` so server and front share one shape. ## Flow ```mermaid flowchart LR effect[Onboarding effect] -- enrichWorkspaceCompany --> checks{creator + work domain?} checks -- no --> unavailable[unavailable] checks -- yes --> throttle{throttle 10/h/workspace} throttle -- limited --> transient[transientError] throttle -- ok --> pdl[PDL GET /company/enrich] pdl --> log[(keyValuePair attempt log)] pdl --> matched[matched] matched --> storage[(localStorage)] storage -- consumed by the stacked workspace-setup PR --> kickoff[hidden kickoff prompt] ``` 1. **Onboarding effect** — mounted app-wide, fires once per session while onboarding is in progress (before workspace activation), guarded by a sessionStorage attempt flag and the cached value. 2. **enrichWorkspaceCompany** — metadata-schema mutation returning a typed `WorkspaceCompanyEnrichmentResult` (`outcome` enum `matched`/`unavailable`/`transientError` + `enrichment` JSON). 3. **Creator + work domain checks** — only the workspace's earliest user, only non-consumer email domains, only when the config flag, API key and `IS_ONBOARDING_AI_CHAT_ENABLED` workspace flag are all on; anything else returns `unavailable` without consuming throttle quota. 4. **Throttle** — token bucket, 10 requests/hour per workspace, the sole cost bound on PDL calls; when limited the mutation returns `transientError` instead of surfacing an error. 5. **PDL call** — `GET /v5/company/enrich` with `website` + `min_likelihood` per the PDL spec; body-level statuses win over HTTP ones, 408/429/5xx map to `transientError`, other failures to `unavailable`. Every attempt past the throttle is recorded (`domain`, the pre-collapse PDL `outcome`, `httpStatus`/`message` when present, `attemptedAt`) in a workspace-scoped `keyValuePair`. 6. **matched** — the PDL payload is mapped to `WorkspaceCompanyEnrichment` through the same sanitizer as client input (all fields length-capped and control-character-stripped; summary 600 chars, 8 tags max) and returned. 7. **localStorage** — the frontend stores only a matched enrichment and never refetches it, making it the only cache; cleared on sign-out. Non-matched outcomes are not persisted; a sessionStorage flag caps retries at one attempt per browser session. 8. **Delivery** — out of scope here. The stacked workspace-setup PR reads the stored enrichment and combines it with the data-model proposal prompt into a single hidden `USER` message when the setup chat starts; it is never injected into the system prompt. Reviewer notes: sending the creator's email domain to a third party at signup is not yet disclosed in onboarding copy. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23199?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. --> |
||
|
|
902bc6db63 |
fix(ai-node) - scope AI agent node database tools to explicitly granted objects (#23400)
## Context An AI agent node scoped to a single object was still loading CRUD tools for the whole workspace, inflating every run's prompt to ~200k tokens (~110k on a standard seed workspace: 146 tools across 19 objects, 18 of them system objects). Two mechanisms caused this: the roles permissions cache force-grants every system object to every role (`isSystem ? true`), and blanket role flags (`canReadAllObjectRecords`, ...) grant all remaining objects. The per-object rows written by the agent Permissions tab were additive on top of that, so scoping an agent had almost no effect on its tool payload. ## What **Backend: explicit grants only for the agent node** - New opt-in flag `requireExplicitObjectGrants` on `ToolProviderContext`, set only by the workflow agent executor. - With the flag, `DatabaseToolProvider` generates CRUD tools exclusively from the role's explicit `objectPermission` rows: no row means no tools, and each verb gate reads the row directly (`canReadObjectRecords` for find tools, `canUpdateObjectRecords` for create/update/upsert, `canSoftDeleteObjectRecords` for delete). A verb left null is not granted; composed defaults and the system force-grant can no longer leak through. Composed permissions are still used for `restrictedFields`. - Explicit rows are read from the `flatObjectPermissionMaps` workspace cache key, fetched in the same `getOrRecompute` call as `rolesPermissions`: no extra query. - Without the flag (chat, MCP, tool index, workspace stats), behavior is unchanged: composed permissions, verified live (`getToolIndex` for an Admin returns the same 245 CRUD tools as before). - Removed the `CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT` guard on `upsertObjectPermissions` so system objects can be granted explicitly. **Frontend: grant system objects from the agent Permissions tab** - The objects picker in the workflow agent side panel ends with a new "System objects" submenu listing all active system objects; picking one opens the same CRUD grant flow as regular objects. - Permissions granted on system objects now resolve their labels in the existing permission list and can be deleted (both previously looked up non-system objects only, which would have hidden such grants). Result: an agent granted one object ships ~10 tools instead of 146, cutting the prompt from ~110k tokens to a few thousand and the per-run cost accordingly. ## Notes - Removing the system-object guard affects the whole upsert path: user roles can also receive explicit system object rows via the API. A `canRead: false` row on a system object now takes effect at the query layer for that role. - The agent role is resolved as the first role of the permission config, matching `getObjectsPermissionsFromRolePermissionConfig` (multi-role is not supported yet). ## Tests - `database-tool.provider.spec.ts`: three new cases for the flag (object without a row emits nothing, partial row emits only granted verbs, absent flag keeps composed behavior even with zero rows, which guards the chat regression). - `object-permission.service.spec.ts`: the system-object case now asserts a successful upsert. - Integration: dropped the failing "system object" upsert case and its snapshot, added a successful system object upsert case. Both suites pass against a live server. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23400?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. --> |
||
|
|
dfea3af778 |
fix(ai-chat): enrich zero-output stream captures and keep client-error exceptions out of Sentry (#23426)
## What & why Two related fixes that clean up Sentry reporting for the AI chat flow. ### 1. Enriched zero-output stream captures The AI chat stream's rejection handler previously skipped only `AbortError` and captured everything else to Sentry as-is. Two problems: - The SDK's bare `NoOutputGeneratedError` carries no troubleshooting context, so the Sentry issues were unactionable (no model, provider, workspace, or conversation size). - Expected interruptions (user abort, `STREAM_INTERRUPTED`) still generated noise. The rejection handler now handles three cases inline: - `AbortError` and `STREAM_INTERRUPTED` are expected interruptions and are not captured. - `NoOutputGeneratedError` is replaced with a single error whose message carries the full context as plain JSON: model, provider, workspace, thread, stream, turn, message count, conversation size, elapsed time, and the underlying stream error - recorded via a new `onError` handler, which also keeps stream-level errors visible in the worker logs. - Anything else is captured unchanged. The stable message prefix and single capture site keep zero-output events grouped separately from raw provider errors in Sentry. ### 2. Keep client-error domain exceptions out of Sentry `BILLING_CREDITS_EXHAUSTED` (a 402, i.e. an expected "user out of credits" condition) was landing in Sentry. Root cause: `CustomException` carries no HTTP status, so the worker/BullMQ path hands the raw exception to `shouldCaptureException`, which can't tell a 4xx client error from a 5xx server error and captures everything. The GraphQL/REST edges convert exceptions first, but background jobs bypass those converters. Fix, mirroring how `HttpException.getStatus()` already works: - `CustomException` gains an intrinsic `statusCode`. - `shouldCaptureException` skips a `CustomException` whose `statusCode < 500`, as a branch symmetric to the existing `HttpException` check. This covers every path, including the worker. - `BillingException` populates `statusCode` from the existing `getBillingExceptionStatusCode` mapping, so credits-exhausted (402) stays out of Sentry while the 500-mapped billing codes are still captured. Exceptions that don't set `statusCode` default to undefined and are captured exactly as before, so other domains are unaffected until they opt in. ## Tests - ai-chat unit suite passes (13 suites, 76 tests). - Existing billing exception handler tests pass. - `typecheck` passes. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23426?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. --> |
||
|
|
ceb699c43f |
Message campaign backfill search field metadata (#23428)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23428?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. --> |
||
|
|
6c66d4c862 |
fix(ai-agent): use a non-workflow base system prompt for programmatic agent runs (#23394)
`AgentAsyncExecutorService` hardcoded `WORKFLOW_SYSTEM_PROMPTS.BASE`, so every caller was told "You are executing as part of a workflow automation" and "your output may be used by downstream workflow nodes". That is only true for the workflow AI-agent action. The `runAgent` API (used by apps such as the call recorder) and agent evaluations got the same framing, which does not describe how they run or where their output goes. The executor no longer asserts its own execution context: `executeAgent` now takes a required `baseSystemPrompt` and each caller supplies its own. - Workflow AI-agent action passes `WORKFLOW_SYSTEM_PROMPTS.BASE` (unchanged behavior) - `runAgent` and evaluations pass the new `AGENT_RUN_BASE_SYSTEM_PROMPT` The param is required rather than defaulted so every call site states its context and no future caller silently inherits the wrong one. Prompt constants are also split one export per file, with the shared tool-usage guidance extracted into `TOOL_USAGE_STRATEGY` so both bases compose it. No GraphQL schema, SDK, or database changes. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23394?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. --> |
||
|
|
897d29b603 |
i18n - translations (#23417)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
1e58c3073c |
Feat/email composer improvements (#23188)
- Move composer to dedicated page - Add test email option - Auto saved as draft can be revisited from `objects/messageCampaigns` later - Campaign stats component https://github.com/user-attachments/assets/9e523116-e79b-496d-9c9d-3887e0c9213f <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23188?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |