cdeebb1a185e4d53186464fc5ffe576dfd3b6fd9
312 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c862a2a43d |
Switch call recorder post-call transcription to Gladia with code switching (#23532)
## What
Switches the Call Recorder app's post-meeting transcription provider
from Recall.ai's built-in transcription (`recallai_async`) to Gladia
(`gladia_v2_async`), with code switching enabled so mixed-language calls
transcribe correctly.
## Changes
- `create_transcript` requests now send `provider: { gladia_v2_async: {
language_config: { code_switching: true } } }` instead of
`recallai_async` with `language_code: 'auto'`. Gladia auto-detects the
spoken language by default, and code switching re-detects it per
utterance for calls that mix languages.
- The provider payload is extracted into a
`RECALL_ASYNC_TRANSCRIPT_PROVIDER` constant so a future
provider-selection variable can slot in without touching the request
code.
- SETUP.md documents the new operational requirement: a Gladia API key
must be added in the Recall.ai dashboard (Transcription > Gladia) for
each region in use, otherwise transcripts fail.
<img width="2810" height="1656" alt="CleanShot 2026-07-30 at 15 00
27@2x"
src="https://github.com/user-attachments/assets/c702ab09-eea8-4c54-8e5a-4941951391c9"
/>
tested on twenty dev recall workspace
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23532?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. -->
|
||
|
|
72322a4d72 |
feat: Slack conversational assistant (#22984)
## Summary Lets workspace members talk to the Twenty CRM agent from Slack — `@mention` the bot in a channel or DM it, and it answers in-thread using the `slack-assistant` agent and its assigned role. ## How it works Slack Events webhook → app route verifies signature → **ack in <3s** and enqueue a `slackAssistantRequest` → worker posts a placeholder immediately, then fetches recent thread/DM history (excluding the current message and placeholder), runs `runAgent`, and updates the placeholder with the answer. After a successful reply, the thread stays subscribed (24h TTL, renewed on each reply) so follow-ups work without re-mentioning. ## App-owned orchestration Protocol + orchestration live in `twenty-apps/public/twenty-slack` (events resolver, enqueue, worker, team claim KV, thread subscription). The server provides shared primitives (app routes, `runAgent`, app KV, connection OAuth). ## Notes - Agent role is bound via `roleUniversalIdentifier` on install. Default **Slack Assistant** role: read/create/update/soft-delete on people, companies, opportunities, notes, and tasks; **workspace members stay read-only**; hard destroy stays off. Admins can tighten the role in Settings. - Setup (signing secret, event subscriptions, scopes) is in the app README. - Long-lived Slack bot tokens (no refresh token) are treated as non-expiring. - Multi-turn: recent Slack thread/DM messages are prepended into the agent prompt. - Replies are non-streaming for now (placeholder + final `chat.update`); progressive streaming is a follow-up. ## Follow-ups - **Streaming replies** — progressive edits while the agent runs. - **Per-user / per-channel permissions** — Slack→Twenty user mapping and optional channel rules (open by default; admins can narrow). - **Other platforms** — Discord/Teams can reuse the same patterns; only Slack protocol is in this PR. ## Screenshots https://github.com/user-attachments/assets/3a72770a-93fa-411d-b4aa-2f741afbcee1 <img width="426" height="686" alt="Screenshot 2026-07-27 at 3 58 38 PM" src="https://github.com/user-attachments/assets/b0a62e7c-c5e4-4c96-9389-5e47d7ef8c77" /> <img width="1053" height="726" alt="Screenshot 2026-07-29 at 12 54 45 AM" src="https://github.com/user-attachments/assets/4e14b3fb-fbe5-4f4d-a380-cc45cc60a01a" /> |
||
|
|
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. |
||
|
|
7b81d9ab83 |
Fix call-recorder REC badge rendering as empty boxes (#23415)
## What was wrong The bot camera image draws a "REC" pill on top of the workspace logo. The label used an SVG `<text>` element, and sharp resolves SVG text through the host's fonts. The runtimes that execute app logic functions ship no fonts, so every character fell back to an empty box: the badge showed "▯▯▯" instead of "REC" in real meetings. It looked fine locally because dev machines have fonts. ## The fix Draw the label as vector outlines instead of text. "REC" is outlined once from Inter SemiBold and stored as an SVG path constant, so the badge renders the same on any host with no font lookup. The pill width is derived from its contents instead of hardcoded, and tests fail if `<text>` or `font-family` ever comes back. <img width="2120" height="1191" alt="CleanShot 2026-07-28 at 19 16 46" src="https://github.com/user-attachments/assets/c5fa0958-35ba-48da-be9c-a6af81ec2fa0" /> 1 -- the bug on prod 2 -- how it looks when its not bugged on prod 3 -- this branches changes <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23415?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. --> |
||
|
|
fea06bdd4b |
v1.5.1 — partners: Discord notification for client briefs + referring-partner attribution (#23344)
**Merge after #23295.** Targets `main`, but must land second: #23295 bumps `1.3.2 → 1.4.0`, and this bumps `1.4.0 → 1.5.1`. Merging this first would leave `main` at 1.5.1 and make #23295's bump conflict and regress the version. `package.json` is the only file the two branches share. App version: **v1.5.1**. ## What this does Posts a Discord notification when a client brief is submitted through the public marketplace form, and records which partner's profile page the brief came from. A visitor can reach the brief form from the marketplace listing page or from a specific partner's profile. Until now that context was lost. This adds a `referredByPartner` relation on Opportunity so the attribution is a queryable CRM fact rather than a line in a chat message. ## How it works `submitClientBrief` resolves the incoming `partnerSlug` to a Partner, sets the relation on create, then posts the embed inline. Inline rather than an `opportunity.created` database trigger, because that event cannot distinguish a brief from a TFT import — both are created by logic functions and both carry `createdBy.source === 'APPLICATION'`. A trigger would need a discriminator like "source is APPLICATION and `tftOpportunityId` is empty", which silently breaks the day a third logic function creates an Opportunity. The cost of going inline is that the Discord call sits in the visitor's request, so it uses a 3s timeout rather than the trigger path's 8s, and every failure is swallowed — a dead webhook can never turn a submitted brief into a failed one. ## Notable decisions - **Slug resolution ignores `validationStage` and `availability`**, unlike the marketplace profile query. If someone submitted a brief from a partner's page, that partner referred it, even if they go unavailable a minute later. Filtering would silently drop real attribution. - **An unresolved slug never fails the brief.** It logs a warning, leaves the relation unset, and still notifies. A brief is a sales lead; losing one over an attribution field the visitor never saw would be a bad trade. - **`referredByPartner` is separate from the existing `partner` field.** One is who sent the lead, the other is who works it. - **The Discord connector moved to `modules/shared/connector/`.** Two domains now need it, and `AGENTS.md` forbids importing logic sideways between domains. `postWebhook` gained `label` and `timeoutMs` parameters; the transport is otherwise unchanged. - Reuses the existing `DISCORD_WEBHOOK_URL` and `PARTNER_APP_FRONTEND_URL` variables — no new configuration to set on prod. ## Permissions `partner.role.ts` locks the new Opportunity field. `configure-partner-rls.ts` treats its skip-list as a closed allowlist of system columns, so an unlocked new field is reported as a discrepancy. Note that Opportunity RLS for partners is `(partnerUser IS me) OR (isListed = true)`, so on a **listed** brief any partner can read `referredByPartner` — i.e. see that a competitor referred it. Called out deliberately; happy to restrict it if that's not wanted. ## Testing 8 unit tests for the embed mapper (partner present/absent, truncation, absent optionals, no email in the payload, inline-row padding) and 4 for the schema. Full suite: 188 passing, lint clean. Verified end to end against a local workspace with a real Discord webhook. All three paths return `ok: true`; the persisted relation was confirmed via GraphQL rather than inferred from the status code: | Submission | `referredByPartner` | |---|---| | valid slug | linked to the partner | | no slug | `null`, embed reads "Marketplace listing" | | unknown slug | `null`, brief still succeeds | ## Follow-up, not in this PR `yarn rls:configure` fails before reaching its field-lock check — its retry path strips `predicateGroups` but the predicates still carry `rowLevelPermissionPredicateGroupId`, so the retry fails identically. Pre-existing and unrelated to this change (`configure-partner-rls.ts` is untouched here), but it means the script cannot currently verify the lock on a fresh workspace. The website side that sends `partnerSlug` is #23351. Until it ships, this is inert: no caller sends the field, and briefs behave exactly as before. Merge this one first — #23351 is the sender, this is the receiver. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23344?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. --> |
||
|
|
1f55234d0b |
fix(call-recorder): leave call when only recording bots remain (#23053)
## Problem Fixes [core-team-issues#2689](https://github.com/twentyhq/core-team-issues/issues/2689). `everyone_left_timeout` only fires when the bot is the sole remaining participant, and Recall counts other recording bots as participants. So when several bots share a meeting, none of them sees itself as alone. Recall does enable `bot_detection` by default, but it ships an empty `matches` list, so the name-based check can never classify anyone. The only detector that actually runs is the behavioural one, at its default 20 minute grace plus 10 minute timeout. A meeting left with only bots therefore stays open for around 30 minutes, and two Twenty bots in the same call never recognise each other at all. This happens when several workspace members are invited to the same meeting and each has the recorder preference on, or when third-party notetakers stay behind after the humans leave. ## What this does Sends a full `automatic_leave.bot_detection` block plus `silence_detection`: - **`using_participant_names`** — the configured recorder name, so co-scheduled Twenty bots recognise each other, plus a list of common notetakers. `timeout: 10`, which is Recall's enforced minimum; their example config shows `5` and the API rejects it. - **`using_participant_events`** — a participant that never speaks nor shares screen is treated as a bot. - **`silence_detection`** — Recall's documented example values (`activate_after: 1200`, `timeout: 300`). Previously unset, so it fell back to Recall's 20 + 60 minute default. Both bot detectors activate 5 minutes after the **meeting start time**, not 5 minutes after the bot joins. The bot joins early by a configurable amount, so anchoring to join time spent the grace period before the meeting existed — at a 10 minute early join, detection would have gone live 5 minutes before the meeting began. `everyone_left_timeout` is unchanged and still covers the ordinary case. Effect: | | before | after | |---|---|---| | Only bots remain | ~30 min | ~5 min after meeting start | | Someone leaves the call open after talking | ~80 min | ~25 min | ## Deferred De-duplicating bots per meeting URL, so several `callRecording`s in one meeting share a single bot instead of each spawning one. `bot_detection` is still needed for third-party bots, so this ships first. --------- Co-authored-by: ehconitin <nitinkoche03@gmail.com> |
||
|
|
75e767f08b |
update exa twenty cli tools (#23379)
as ttitle |
||
|
|
b94a889bcb |
Organize public apps properly (#23376)
remove "twenty-" prefixes from public folders and package names <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23376?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. --> |
||
|
|
e631c986a1 |
v1.4.0 — partners: auto-link partner user on workspaceMember.created (#23295)
**App version:** `1.4.0` (partners app — `packages/twenty-apps/internal/twenty-partners`) ## What Adds partner onboarding auto-linking: when a `workspaceMember` is created (invite signup), a DB-event-triggered logic function resolves the partner by the member's email and stamps `partnerUser` across the partner and its cascade (person, company, links, services, content, applications). ## Key design decision — data-linking only, no role assignment The trigger **does not** assign the Partner role. A logic function runs as an app **agent**, with no user session; `updateWorkspaceMemberRole` is guarded by `UserAuthGuard` + `AuthWorkspaceMemberId` and is unreachable from an agent, so the mutation silently no-ops regardless of permission flags. The dead role code (`ensure-partner-role` service, its role query/mutation, and the role mocks) is removed so the trigger's responsibility is unambiguous: resolve partner by email → link `partnerUser` cascade with retry-on-partial-failure. Role assignment, if wanted, belongs on the invite path (`sendInvitations` accepts a `roleId`), not the trigger. ## Changes - `on-workspace-member-created.logic-function.ts` — DB-event trigger on `workspaceMember.created`; skips internal (`@twenty.com`) and unmatched emails - `resolve-partner-by-email` / `link-partner-user` services + typed `graphql/` operations for the cascade - `normalize-invite-email` util - `partnerUserLinkedAt` field on Partner - Seed: one contact `Person` (with `partnerId` + email) and one `Company` per partner so onboarding is testable via a seeded invite email; drops the `person.city` write removed in SDK 2.25 that broke `yarn seed` ## Verification - Unit: **173/173 pass** (27 files) · `tsc --noEmit` clean · `oxlint` 0 warnings/0 errors - End-to-end: invited + signed in a seeded partner (`lena@act-education.example`) on the workspace subdomain; the trigger linked the member to the **Act Education** partner and the self-service **My Profile** page rendered the linked profile (`POST /s/my-partner-profile → 200`) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23295?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. --> |
||
|
|
302f46f0ea |
fix: bump brace-expansion to 5.0.8 in app lockfiles (Dependabot) (#23346)
## Summary Bumps **brace-expansion -> 5.0.8** in the three app lockfiles whose copy sits on the 5.x line, clearing **GHSA-mh99-v99m-4gvg** (high, vulnerable `<= 5.0.7`) on those manifests: - `examples/hello-world` (`^5.0.2`) - `examples/postcard` (`^5.0.5`) - `internal/self-hosting` (`^5.0.5`) All three are caret ranges, so a recursive `yarn up -R brace-expansion` lifts them with **no resolution and no `package.json` change**. ## Why the fixtures are not included This advisory declares a single vulnerable range, `<= 5.0.7`, which spans **every** major line - so the `brace-expansion@2.1.2` copies in `seed-dependencies` and `common-layer-dependencies` are flagged as well. But **2.1.2 is the last 2.x release** (1.x likewise ends at 1.1.16), and the only patched version is **5.0.8**. Those consumers declare `^2.0.1` / `^2.0.2`, which caps below 3.0.0, so there is no in-range fix: clearing them would mean forcing a cross-major jump from 2.x to 5.x via a resolution, which is a behavior risk rather than a mechanical lift. Same situation for the root alert ([1765](https://github.com/twentyhq/twenty/security/dependabot/1765)), where `nx` pins `brace-expansion` 5.0.6 exact. ## Verification - brace-expansion resolves to **5.0.8** in all three lockfiles. - `yarn install --immutable` passes in each. - 5.0.8 published 2026-07-23, clears the 3-day npm age gate. |
||
|
|
a6b36422f9 |
Fireflies: upgrade to twenty-sdk 2.23 (#23349)
Fireflies was skipped by both SDK bump sweeps (#23124, #23165) and sat on `twenty-sdk ^2.18.0` with no `engines.twenty` floor, while the rest of the published set moved to `2.23.0-alpha.2`. - `twenty-sdk` / `twenty-client-sdk` `^2.18.0` -> `2.23.0-alpha.2` - adds `engines.twenty: ">=2.23.0"` No source changes needed: the 2.19 identifier migration (#22601) only affected apps referencing a standard object's system-field identifier, defining a relation into a standard object, or calling the field-UID derivation helper. Fireflies does none of those. The `engines.twenty` floor means the app integration job needs a server image at 2.23+, so it may fail on version mismatch rather than an app defect, as in #22601. |
||
|
|
ed95b8cfde |
fix: bump postcss to 8.5.22 across app lockfiles (Dependabot) (#23340)
## Summary Bumps **postcss -> 8.5.22** in the 13 twenty-apps lockfiles that carry it transitively, clearing **GHSA-r28c-9q8g-f849** (high) on those manifests: path traversal in previous source map auto-loading (`sourceMappingURL`) leading to arbitrary `.map` file disclosure, vulnerable `<= 8.5.17`. Apps covered: document-generator, hello-world, postcard, self-hosting, twenty-partners, call-recorder, people-data-labs, twenty-discord, twenty-exa, twenty-fireflies, twenty-last-contact, twenty-linear, twenty-slack. Every app reaches postcss through a caret range (`^8.5.15`), so a recursive `yarn up -R postcss` lifts it in each project with **no resolution and no `package.json` change** - the diff is 13 `yarn.lock` files and nothing else. Yarn resolves to **8.5.22**, the latest in range (above the 8.5.18 fix floor). ## Not included The **root lockfile** carries the same advisory but its postcss copies are held by exact pins - `next` (8.4.31 in every stable release, including 16.2.11) and `@mintlify/common` (8.5.14, unchanged in its latest) - so no `yarn up` reaches it. That one needs a scoped resolution and is handled separately. ## Verification - postcss resolves to **8.5.22** in all 13 lockfiles; nothing at or below 8.5.17 remains. - `yarn install --immutable` passes in each of the 13 projects. - 8.5.22 published 2026-07-22, clears the 3-day npm age gate. |
||
|
|
155636d7d9 |
fix: bump tar to 7.5.21 across app lockfiles (Dependabot) (#23332)
## Summary Bumps **tar -> 7.5.21** in the 13 twenty-apps lockfiles that carry it transitively, clearing **GHSA-r292-9mhp-454m** (medium) on those manifests: uncontrolled recursion in `mapHas`/`filesFilter` allows an uncatchable stack-overflow DoS via a crafted long-path tar with member selection, vulnerable `<= 7.5.20`. Apps covered: document-generator, hello-world, postcard, self-hosting, twenty-partners, call-recorder, people-data-labs, twenty-discord, twenty-exa, twenty-fireflies, twenty-last-contact, twenty-linear, twenty-slack. Every app reaches tar through a caret range (`^7.5.4`), so a recursive `yarn up -R tar` lifts it in each project with **no resolution and no `package.json` change** - the diff is 13 `yarn.lock` files and nothing else. ## Not included - **Root lockfile**: same advisory, shipped separately in #23330. - **`application-package/constants/seed-dependencies`**: the 14th manifest with this advisory. Its `yarn.lock` is checksum-coupled to `DEFAULT_YARN_LOCK_CHECKSUM`, so it moves in its own PR with the constant regenerated alongside. ## Verification - tar resolves to **7.5.21** in all 13 lockfiles; nothing below remains. - `yarn install --immutable` passes in each of the 13 projects. - 7.5.21 published 2026-07-21, clears the 3-day npm age gate. |
||
|
|
0d876eb714 |
fix: lift axios/tar/brace-expansion/body-parser across app lockfiles (Dependabot) (#23267)
## Summary Sweeps the **twenty-apps lockfiles** for this week's advisory wave: recursive `yarn up` for **axios, tar, brace-expansion, body-parser** in each of the 12 apps with open Dependabot alerts (hello-world, postcard, self-hosting, twenty-partners, call-recorder, people-data-labs, twenty-discord, twenty-exa, twenty-fireflies, twenty-last-contact, twenty-linear, twenty-slack). All moves fit the declared ranges (apps carry these transitively via `twenty-sdk`, whose `axios ^1.16.0` and deep tar/brace chains are carets), so the diff is **lockfile-only** across all 12 manifests - no resolutions, no `package.json` changes. axios -> 1.18.x, tar -> 7.5.20 (critical GHSA-23hp-3jrh-7fpw chain), brace-expansion -> 1.1.16 / 2.1.2 / 5.0.7, body-parser -> 1.20.6 / 2.3.0. The second commit narrows scope to apps only: the twenty-server fixture projects (seed-dependencies, common-layer-dependencies) move to a dedicated PR because seed-dependencies' yarn.lock is checksum-coupled to `DEFAULT_YARN_LOCK_CHECKSUM` in `get-default-application-package-fields.util.ts`; it also drops accidentally committed `.yarn/install-state.gz` artifacts. ## Deliberately not covered - **sharp**: every path is minor-locked at `^0.34.5` (including twenty-sdk latest) - separate PR bumping twenty-sdk's range. - **react-router / react-router-dom**: no fixed release on the 6.x line (fix is the v7 major); tracked separately. ## Verification - Vulnerable-version scan across all 12 lockfiles: no axios <1.18, tar <7.5.19, brace-expansion below 1.1.16/2.1.2/5.0.7, or body-parser below 1.20.6/2.3.0 remains. - `yarn install --immutable` passes in each app. - All fix versions clear the 3-day npm age gate. |
||
|
|
940d150775 |
chore(self-hosting): upgrade to latest twenty CLI tooling (#23279)
## What Upgrades the internal `self-hosting` app to the latest Twenty CLI tooling, bringing it in line with the other actively-maintained apps in the monorepo. - `twenty-sdk`: `2.19.0-alpha.1` → `2.23.0-alpha.2` (dependency + devDependency) - `twenty-client-sdk`: `2.19.0-alpha.1` → `2.23.0-alpha.2` (dependency + devDependency) - `engines.twenty`: `>=2.19.0` → `>=2.23.0` - Regenerated `yarn.lock` to match. The `twenty` CLI ships inside `twenty-sdk`, so this pulls the app onto the same CLI version every other recently-updated app (call-recorder, people-data-labs, twenty-partners, real-estate, last-contact, postcard) already uses. ## Verification - `yarn typecheck` passes - `yarn lint` passes (0 warnings, 0 errors) - `yarn test:unit` passes (3/3) Integration tests (`yarn test`) require a running Twenty server and were not run in this environment. ## Notes Scope is limited to the CLI/SDK tooling. Framework deps (React 18) were left untouched since they are not tied to the CLI version and vary across apps. --- _Generated by [Claude Code](https://claude.ai/code/session_01HUPkxerLhethaP9Lw6qDyd)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23279?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. --> |
||
|
|
d1c6b8ee72 |
Show relation record labels instead of UUIDs in dashboard charts (#23163)
https://github.com/user-attachments/assets/d012a013-2c90-49a1-a27e-b8e4b684a84f Charts grouped by a relation without a sub-field rendered raw FK UUIDs on axis ticks, legends and tooltips. The server now batch-resolves the grouped record ids to their label identifier through a permission-scoped query and formats every bucket with the record's display name. Unresolvable records (deleted or not readable) render as Unknown and their ids are stripped from the response payload. Same-named records get an ordinal suffix so their buckets don't merge. Covers bar, line and pie, plain and morph relations. ```mermaid flowchart TD A["Dashboard widget load"] --> B["Chart data service<br/>(bar / line / pie)"] B --> C["executeGroupByQuery:<br/>group by relation FK id,<br/>ORDER BY target label identifier,<br/>scoped to source object permissions"] C --> D["filterOutEmptyChartBuckets"] D --> E{"Bare relation axis?<br/>(no sub-field)"} subgraph RL["ChartRelationLabelService.resolveRelationLabels"] direction TB G1["Collect distinct record ids<br/>per target object"] --> G2["Batch SELECT label identifier columns,<br/>scoped to TARGET object permissions"] G2 --> G3["buildRawLabelByRecordId:<br/>display name per record"] G3 --> G4["buildUniqueRelationLabels:<br/>suffix duplicates, Unknown for unresolved"] end E -- No --> H["formatDimensionValue per bucket"] E -- Yes --> G1 G4 --> H H --> I["Strip unresolved ids from<br/>formattedToRawLookup"] I --> J["Chart DTO to frontend"] ``` The chart settings sub-field dropdown gains a Record option to group by the related record itself, and now only offers sub-fields the backend accepts (system fields like a workspace member's updatedBy were selectable but rejected at query time). Chart-data errors are now logged server-side. Also fixes two latent bugs on this path: sorting a bare-relation chart by field threw `Cannot orderBy unknown field: agentId`, and the pie chart truncated slices before sorting. The AI dashboard tool guidance and the seeded dashboards no longer force the sub-field workaround. The group-by query orders buckets by the related record's label identifier at the database level (the engine now accepts ordering by a target field when grouping by its id), so with more than 100 distinct related records the surviving buckets match the label order. |
||
|
|
18fb0946e6 |
v1.4.0 — Raise partner bar: Twenty experience fields + triage (#23224)
## Summary **Version:** `twenty-partners` **v1.4.0** (minor — new Partner fields + apply contract) - Add Partner fields `twentyExperience`, `twentyExperienceNotes`, `twentyExperienceProofLink` and persist them from `submit-partner-application` (≥200-char narrative at API boundary) - Surface Twenty experience on applications / validated / per-stage triage views and the Partner record side panel (drop empty Introduction from that panel) - Add pure Tally CSV match/map helpers (ops import script stays outside the repo) for backfilling existing partners by `partnerId` **Companion PR (website):** #23223 — Experience step on apply + thank-you without Cal. ## Test plan - [ ] `yarn twenty apply -r <remote>` on a workspace — Partner gains the three experience fields - [ ] Website apply (with #23223) persists milestones / notes / proof link on create and email-linked update - [ ] Applications + Validated views show experience columns; record side panel lists experience fields - [ ] `yarn lint` clean; `yarn test:unit` covers schema + map-tally helpers - [ ] After Tally campaign: dry-run then apply CSV import via local ops script under `~/twenty/docs/superpowers-specs/raise-bar-import/` |
||
|
|
9d8ce7c325 |
v1.3.2 — Modularize partners app into vertical-slice modules/ (#23168)
**Version:** `twenty-partners@1.3.2` (patch — internal refactor, no
visible behavior change)
## What & why
Reorganizes the `twenty-partners` SDK app from a flat, type-first layout
(`src/{objects,fields,views,logic-functions,front-components,…}`) into a
**vertical-slice** layout under `src/modules/<domain>/<feature>/`. Files
that
change together now live together; each SDK entrypoint is a thin
discoverable
shim over a service + graphql-ops + mapper/connector layer.
This is a pure structural refactor — **no object, field, view, enum,
logic
function, trigger, role, or application variable changed.**
## Final layout
```
src/modules/
shared/ http · services · graphql · utils · front-components · navigation-menu-items (cross-domain nav folders)
opportunity/ fields·view-fields·views·navigation-menu-items·page-layouts·constants + intake/ + matching/
partner/ objects·fields·constants·utils + directory/ · self-service/ · marketplace/ · application-intake/ (Discord connector/)
application/ objects·fields·views·navigation-menu-items·page-layouts + services · graphql
```
Every `defineLogicFunction` entrypoint is now a thin
`*.logic-function.ts`
(all < 40 lines) at its domain/feature root, delegating to a
`*.service.ts`;
graphql operations live in `graphql/{queries,mutations}/`, pure
transforms in
`mappers/`, outbound APIs (the Discord webhook) in `connector/`, pure
helpers
in `utils/`.
## Safety — the load-bearing invariant
The server diffs app primitives by `universalIdentifier`, so a
changed/dropped
UUID would drop-and-recreate the object on prod (data loss). This branch
holds
that line:
- **887 `universalIdentifier`s byte-identical** to the branch base
(every
relocation is a `git mv`; every extracted entrypoint keeps its original
UUID/name/trigger verbatim). Re-verified byte-identical across the
rebase.
- `yarn twenty dev --once` against a live workspace = **"No changes.
Twenty
metadata matches your manifest."**, confirmed idempotent on a second run
—
the whole refactor is a metadata no-op (zero create/delete/identity
change).
- Every extracted graphql op was verified **byte-identical** to its
original
(args, `first:` caps, pagination, selection sets), and the
partner-application
Discord embed's deliberate PII omission (no email / hourly rate) is
preserved.
## Rebased onto latest `main`
This branch is rebased onto `main` (`d20e5378fd`) and now carries main's
`twenty-sdk` / `twenty-client-sdk` **2.23.0-alpha.2** bump.
Note for reviewers: main had independently bumped this package to
`1.3.1`, so
the original `1.3.0 → 1.3.1` commit here was redundant and git dropped
it during
the rebase (`patch contents already upstream`) — with **no textual
conflict**,
since both sides wrote the same version string. The bump is therefore
now
**`1.3.2`**. The rebase touched only `package.json` and `yarn.lock`;
**every line
of refactored source is byte-identical** to the pre-rebase tree.
## Verification
All run on the rebased tree, against SDK `2.23.0-alpha.2` and a live
Twenty
server `v2.23.2`:
| Check | Result |
|---|---|
| `universalIdentifier` set | 887, byte-identical |
| `yarn twenty dev --once` | "No changes" (idempotent on re-run) |
| Typecheck | pass |
| `yarn lint` | 0 warnings, 0 errors (287 files) |
| `yarn test:unit` | 158/158 (23 files) |
| `yarn test:integration` | 45/45 (13 files) |
## Also in this PR
- **Architecture convention doc** — `AGENTS.md` (+ a one-line
`CLAUDE.md` pointer)
at the package root documents the vertical-slice conventions this
refactor
establishes: the layout, the dependency rule (`logic-function → service
→
graphql/connector`), file naming, connector = outbound-only (inbound
webhooks
are logic-functions), and the UUID invariant. It ships here so the doc
and the
structure it describes land together.
- **`modules/shared/`** dedup: the secret-guarded intake envelope, the
find-or-create-company/person helpers + their graphql ops, `collectAll`
pagination, `http-url`/`strip-markdown`/`is-non-empty-string` utils.
- **Vitest configs collapsed** into one `vitest.config.ts` with `unit` +
`integration` projects (`yarn test:unit` / `yarn test:integration`).
- Cross-domain nav folders (`pipeline-folder`,
`partner-workspace-folder`)
hoisted to `modules/shared/navigation-menu-items/`.
## Deferred (non-blocking, tracked follow-ups)
- Add direct unit tests for the shared `collectAll` / `isNonEmptyString`
utils
(currently covered indirectly).
- Move `submit-client-brief`'s zod schema out of its mapper file into
its own
schema file (mirroring the partner side).
- Route `stamp-partner-user-on-child` through the shared self-service
mutation ops.
- `find-partner-by-member.ts` is duplicated identically in the
`application` and
`self-service` domains; a candidate to hoist into `modules/shared/`.
|
||
|
|
e0debf87a7 |
fix(call-recorder): listen proper updated fileds event (#23135)
## Context Around meeting-end peaks (~6pm), Recall/Svix delivers event bursts for every recorded call across the 700+ workspaces the app is installed on. Each delivery was processed synchronously in the API request path, and internal failures surfaced to Svix as non-2xx, so it redelivered — a self-feeding storm of 500s and latency that only stopped when the webhook endpoint was disabled. ## What changed With #23134, server-route dispatch defaults to **queued** server-side: the API acks Svix with a 202 right after signature verification, `process-recall-webhook` runs on the worker queue, and failed runs retry there (resolver `retryLimit`, default 3). The resolver needs no change at all — `recall-webhook.ts` is back to main, and no SDK update is required. Remaining app changes: - `schedule-recall-bot-on-call-recording-update` declares `updatedFields` (the pending-transition fields) on its `callRecording.updated` trigger, so the server drops the app's own scheduling-progress and artifact writes **before** spawning a full execution instead of executing and returning "skipped". The in-handler check stays as a fallback. - Version bumped to 1.5.0. - Code comments introduced by earlier revisions of this PR removed per review. ## Tests - New test pins the trigger's `updatedFields` declaration. - `yarn test:unit` (507 tests), `yarn lint`, `yarn typecheck` all green. ## Notes - The `call-recorder (dockerhub-latest)` CI leg fails because main already requires `twenty >= 2.23.0` while the latest published image is 2.22.0 — pre-existing, clears when 2.23.0 images publish. - The 250s `import-call-recording-artifacts` route still runs in an API request slot behind the fire-and-forget own-route POST; moving it fully off the request path is a follow-up. |
||
|
|
16e7db8577 |
PDL react 19 (#23169)
Started to face in version 1.07 on staging 2.23.0 ``` Failed to load front component: Cannot read properties of undefined (reading 'ReactCurrentBatchConfig') ``` |
||
|
|
8d943e1f68 |
Bump to alpha 2 all published apps (#23165)
Related https://github.com/twentyhq/twenty/pull/23155 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23165?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
623aac5a56 |
Show agent names on real estate dashboard charts (#23146)
Follow-up to the real estate demo app: the Agency Overview dashboard's "Listings by agent" and "Showings by agent" bar charts grouped by the agent relation, which rendered the agent's UUID on the axis. Adding `primaryAxisGroupBySubFieldName: 'name.firstName'` groups by the agent's first name instead, so the charts show agent names (Emma, Lucas, Chloe, Louis). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23146?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
c59df4cddd |
Upgrade remaining official apps to 2.23 alpha (#23124)
The sdk does not provide system field anymore at all ( including relation ) if you don't upgrade you'll get a deterministic universal identifier collision from previously provision and now side effect resulting ones <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23124?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. --> |
||
|
|
9a374c4b6d |
Add real estate demo app (#23111)
## What A new app under `packages/twenty-apps/internal/real-estate` that seeds a real-estate demo workspace: buyers, sellers, agents, property listings, showings, and an opportunity pipeline, with role-based access. ## Data model - **Property** (custom object): address, price (currency), status (coming soon / active / under offer / sold), type, beds/baths/surface, photos, `listingAgent` and `sellerContact` relations to Person. - **Showing** (custom object): scheduledAt, status, feedback, interest rating, and `property` / `buyer` / `agent` / `opportunity` relations. - **Person** (standard, extended): `personType` (Buyer / Seller / Agent), budget min/max, pre-approved, desired area. - **Opportunity** (standard, extended): `buyerStage` pipeline (completing profile → showing → offer made → closing → won → lost), and `buyer` / `seller` / `property` / `showings` relations. ## Views - **Buyer Pipeline** — kanban on Opportunity grouped by buyer stage, one card per buyer, scoped to real-estate deals. - **Available (by price)** — properties sorted by price desc, excluding sold. - **Agents** / **Buyers** — filtered Person views. - Record-page layouts for Opportunity and Showing so the relations render on the detail pages. ## Roles - **Broker** — full access (default). - **Agent** — Property / Showing / Person / Note / Task, no Opportunity access. - **Seller** — read-only on their listing and its showings. ## Seeding A synchronous post-install logic function seeds 4 agents, 12 sellers, 12 buyers, 30 properties across 5 cities, 24 showings, and 12 opportunities (one per buyer), all wired through the relations. --------- Co-authored-by: prastoin <paul@twenty.com> |
||
|
|
2ef7b7824e |
Upgrade call-recorder, people-data-labs, last-contact and partners apps to twenty-sdk 2.23.0-alpha.1 (#23098)
## What Upgrades the two breaking-change-prone apps to `twenty-sdk` / `twenty-client-sdk` `2.23.0-alpha.1`, and adds the server-side hook that lets the 2.23 upgrade install them: - **people-data-labs** - **partners** Follows up on #22882 (System side effect relations), which re-derived the system relation field universal identifiers name-free and shipped `getSystemRelationFieldUniversalIdentifier` in the SDK. ## How - **people-data-labs**: bump the SDK to `2.23.0-alpha.1`. The enriched views temporarily hardcoded the new system relation identifiers with a TODO because the SDK still embedded the old values; now that the name-free identifiers ship in `2.23`, derive them from `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.{company,person}.fields.{noteTargets,taskTargets,attachments,timelineActivities}.universalIdentifier` (identical to the previously pinned values, verified). Engine already pinned `twenty >=2.23.0`; app stays `1.0.7` (manifest unchanged). - **partners**: bump the SDK to `2.23.0-alpha.1`. The partner role references `opportunity.fields.{taskTargets,noteTargets,attachments,timelineActivities}` universal identifiers, which the SDK now resolves to the `2.23` name-free values. Pin `engines.twenty >=2.23.0` and bump the app to `1.3.1`. - **server**: add an opt-in `skipWorkspaceCompatibilityCheck` to the install/upgrade path. The `upgrade-people-data-labs-application` 2.23 command runs mid-upgrade, before the workspace is marked as having completed 2.23, so the workspace-compatibility check would otherwise reject installing `1.0.7` (`engines >=2.23.0`). The server is already on 2.23, so the command passes the flag to install `1.0.7` and close the desync window. Version-progression (downgrade/same-version) checks still run. - **call-recorder** and **last-contact** are intentionally left unchanged (reverted): they don't define custom objects and don't reference the system relation identifiers, so they aren't breaking-change-prone and need no SDK bump. ## Breaking change constraints - **people-data-labs** and **partners** reference system relation identifiers that only exist on a `2.23` server, so both pin `engines.twenty >=2.23.0`. Their `dockerhub-latest` integration leg is red by design until a >=2.23 server image is published (same accepted state as #22882); the `local` leg is green. ## Validation - Regenerated the app lockfiles against the published `2.23.0-alpha.1`. - `people-data-labs` typechecks cleanly against the real `2.23` SDK types. - CI: people-data-labs and partners green on `local`, red on `dockerhub-latest` by design; server/SDK/all other checks green. - Rebased onto latest `main`. |
||
|
|
a0e8d48656 |
Reduce call-recorder recovery crons to daily to relieve production (#23099)
## Context Call Recorder is installed on 700+ workspaces and its two recovery crons run every 15 minutes with the same pattern in every workspace, so all executions land on the same minute boundaries and impact production. The `callRecording.updated` event trigger (#23014) now covers the fast path within seconds; these crons are only backstops for crashed creations and missed webhooks. ## What changed Pattern updates only, no logic changes: - `process-pending-call-recording-requests`: `*/15 * * * *` -> `0 3 * * *` - `reconcile-stale-bot-state`: `*/15 * * * *` -> `30 3 * * *` The daily times are staggered half an hour apart from the existing daily crons (04:00 upcoming-events sweep, 04:30 orphaned-bots cleanup) so the four daily jobs never coincide. ## Notes - Recovery latency for rows missed by the event trigger becomes up to 24h instead of 15min, which is acceptable for backstops (the 7-day convergence lookback is unaffected). - Cron patterns live in installed manifests, so existing installations pick this up on app upgrade only. - The daily herd across workspaces at 03:00/03:30 remains synchronized until generic cron spreading lands server-side (#23088 covers only the `*/5` and `*/15` patterns). --------- Co-authored-by: martmull <martin@twenty.com> |
||
|
|
1be5a0e54a |
System side effect relations (#22882)
Closes twentyhq/core-team-issues#2667 ## What Default relations to the standard relation objects (`timelineActivities`, `attachments`, `noteTargets`, `taskTargets`) are now fully owned by the **metadata side-effect engine**. Neither the API transpilers nor the SDK manifest builder provision them anymore: any object creation, rename or deletion — regardless of the caller — goes through the same engine handlers. ## Why - Provisioning was duplicated across the API path and the SDK manifest builder, with diverging behavior. - Universal identifiers of relation fields were derived from object **names**, so renaming an object mutated them and forced lossy delete+create cycles on manifest sync. ## How ### Engine-owned lifecycle (side-effect handlers) - `objectSystemRelationsOnCreate`: provisions the 8 forward/reverse relation fields (+ join column indexes) when an object is created. - `objectSystemRelationsOnUpdate`: renames the reverse morph fields (`target<ObjectName>`) when their host object is renamed — a lossless `fieldMetadata.update`. - `objectSystemSideEffectsOnDelete`: cascades deletion of engine-owned fields/indexes when the object is deleted. - The API transpilers and the SDK `buildManifest` no longer inject these fields; `isSystemSideEffect: true` marks engine-owned entities, guarded by a granular property allowlist (only `isActive` is user-editable) and excluded from manifest deletion inference. ### Name-free deterministic universal identifiers New `getSystemRelationFieldUniversalIdentifier({ applicationUniversalIdentifier, objectUniversalIdentifier, relationTargetObjectUniversalIdentifier })` in `twenty-shared`, exported from `twenty-sdk/define`. The identifier is keyed on the two **object** identifiers instead of field names (direction encoded by argument order), so object renames never mutate relation field identifiers. It cannot collide with the name-based `getFieldUniversalIdentifier` derivation (field names cannot contain `:`). ### twenty-standard re-owned All 48 forward/reverse system relation field declarations in `STANDARD_OBJECTS` now pin the derived name-free identifiers (computed inline via the shared util) and carry `isSystemSideEffect: true`, with labels/icons declared explicitly (translated via `msg`). `twenty-standard` is projected as if the engine had generated these fields itself. ### 2.23 upgrade commands - `reconcile-system-relation-field-universal-identifier`: structurally matches existing default relation fields per workspace and backfills the derived universal identifiers, `isSystemSideEffect` flags, and standard labels/icons. - `upgrade-people-data-labs-application`: upgrades installed PDL apps to `1.0.7` right after the backfill to close the desync window (its views reference the re-derived identifiers). ### Misc - `people-data-labs` `1.0.7`: views temporarily pin the new derived identifiers (TODO: import from the next released `twenty-sdk`). - `UpgradeStatusModule` split out of `UpgradeModule` so the application module cluster can consume upgrade status/migration services without importing the versioned command bundles (fixes a require cycle that crashed boot). - Docs: `system-fields.mdx` documents the system relation fields and their resolver; `sync-and-recovery.mdx` plan example no longer shows auto-injected relations. ## Known red CI `people-data-labs (dockerhub-latest)` fails by design until the 2.23 server image is published: the app pins the new identifiers which only exist on a 2.23 server. The `local` leg (server built from this branch) is green. ## System fields are no longer manifest-authorable (accepted regression) The manifest converter no longer derives `isSystem` / `isSystemSideEffect` from field names. Reserved-system-named manifest fields (`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`) are now skipped at conversion time when they carry the exact derived universal identifier (keeps manifests built with older SDKs installable), and rejected with `INVALID_INPUT` when they pin any other identifier. System fields are therefore fully engine-canonical: nothing a manifest carries can produce a system-flagged entity anymore. **Accepted regression**: a manifest can no longer influence system field properties at all. Previously a (legacy) re-declaration could shape them at creation — which actually produced broken system fields, e.g. a nullable, non-unique `id` — and could still toggle the allowlisted `isActive` / `universalSettings` afterwards. We consider this acceptable for now: per-app granularity over system fields will be reintroduced later through the **override framework**, which will also settle update semantics by forbidding direct updates over `isSystemSideEffect: true` entities and expressing divergence as overrides. `isSystemSideEffect`-only entities (the default relation fields provisioned by this PR) still have no engine-level update guard (see Follow-up below); that part is unchanged and also lands with the overrides refactor. ## Follow-up `isSystemSideEffect` field update/delete guards intentionally live at the API layer (`sanitize-raw-update-field-input.ts`, `from-delete-field-input-...util.ts`) rather than in the engine-level `FlatFieldMetadataValidatorService`. Moving them into the validator requires threading operation-origin (direct field mutation vs engine cascade) through the migration matrix, otherwise legitimate object rename/delete cascades (which carry `isSystemBuild=false`) would be rejected. Tracked in twentyhq/core-team-issues#2671. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22882?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. --> |
||
|
|
1b5e974629 |
feat(call-recorder): add copy-to-clipboard buttons for transcript, summary, and video link (#23052)
## Context Closes twentyhq/core-team-issues#2692. Adds copy-to-clipboard actions to the call recorder app so users can quickly share a call's transcript, summary, and video. ## What changed - **Copy transcript** button in the *Recording and Transcript* widget header. Copies the transcript as plain text with resolved speaker display names and timestamps (mirroring what is shown on screen). - **Copy video download link** button in the same header. Copies the signed video file URL. - **Copy summary** button in the *Summary* widget header. Copies the summary markdown. Each button is powered by a new reusable `CopyToClipboardButton` component that writes to the clipboard, briefly swaps to a check icon for feedback, and surfaces a success/error snackbar. Buttons are disabled when there is nothing to copy (no transcript / video / summary, or while loading). A `buildTranscriptPlainText` utility turns parsed transcript entries into shareable text, with participant display names preferred over raw diarized speaker labels. ## Screenshots The *Recording and Transcript* header now shows a copy-transcript and a copy-video-link button, and the *Summary* header shows a copy-summary button. | Light | Dark | | --- | --- | | <img width="426" src="https://raw.githubusercontent.com/twentyhq/twenty/claude/issue-2692-screenshots/.github/pr-screenshots/2692/call-recorder-copy-buttons-light.png" /> | <img width="426" src="https://raw.githubusercontent.com/twentyhq/twenty/claude/issue-2692-screenshots/.github/pr-screenshots/2692/call-recorder-copy-buttons-dark.png" /> | ## Tests - New unit tests for `buildTranscriptPlainText` (speaker/timestamp formatting, missing timestamps, participant name resolution). - Full app unit suite passes (491 tests), plus typecheck and lint. |
||
|
|
5bf3472eb9 |
chore(twenty-exa): bump to 0.2.0, add marketplace metadata and Twenty version floor (#23063)
## What Prepares the Exa app (`@twentyhq/twenty-exa`) for a fresh npm release. - Bump `version` `0.1.0` → `0.2.0` - Add `engines.twenty: ">=2.19.0"` so older servers don't install an incompatible build - Add marketplace metadata in `defineApplication()`: `category: 'Search'`, `websiteUrl`, `termsUrl`, `emailSupport`, `issueReportUrl` (matching the values used by the other `@twentyhq/*` apps) ## Why The version currently published on npm is the **unscoped** `twenty-exa@0.1.0`, which predates several SDK breaking changes. The in-repo source has since migrated to `twenty-sdk@~2.16` and `exa-js` v2: - `chargeCredits` now imported from `twenty-sdk/billing` (was a local util) - logic function uses `toolTriggerSettings.inputSchema` (was `isTool` + `toolInputSchema`) - schema type imported from `twenty-sdk/logic-function` (was `twenty-shared/logic-function`) - `category` enum updated to the exa-js v2 union (removed `github`/`tweet`/`linkedin profile`, added `people`) So the published build is effectively broken on current servers. This PR readies a `0.2.0` release under the standard scoped name `@twentyhq/twenty-exa`. The app's `universalIdentifier` is unchanged (`2b7f4a2e-9c4b-4a11-b63c-2e5e7d3f5a9a`), so Twenty treats this as the **same app** and upgrades existing installs in place — the name change (unscoped → scoped) is only an npm-registry concern. ## Changes - `packages/twenty-apps/public/twenty-exa/package.json` - `packages/twenty-apps/public/twenty-exa/src/application.config.ts` ## Testing - `yarn typecheck` — pass - `yarn lint` — pass (0 errors) - `yarn twenty dev:build` — builds a valid `@twentyhq/twenty-exa@0.2.0` tarball ## Follow-up (not in this PR — npm/ops, needs auth) - Publish `@twentyhq/twenty-exa@0.2.0` to npm (`yarn twenty app:publish`) - Deprecate + de-keyword the old unscoped `twenty-exa` so only one package feeds the shared `universalIdentifier` on catalog sync <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23063?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. --> |
||
|
|
e6c6cccafa |
v1.3.0 — Partner workspace self-service (glowup app) (#22929)
## Glowup — app · v1.3.0 (Release ② of the brief + glowup rollout) Partner **workspace self-service**: partners manage their own profile, links, services, and case studies from inside the CRM (new objects + record-page views + a "My Profile" self-service front-component). Evolved superset of the closed #22470 (v1.3.0). App-only — **0 website files**. Version **1.3.0** (prod is currently 1.2.10). SDK **2.19.0**. Supersedes **#22470** (closed). ### Verified locally Provisioned a throwaway workspace, synced the schema, seeded, and exercised the full surface end-to-end: marketplace + public profiles render live; **partner self-service pages** (My Profile / My Case Studies / links / services) load and save when acting as a partner user; both intake forms (partner application + client brief) submit successfully. `oxlint` 0/0, typecheck clean. ### Notes - Committed `APPLICATION_UNIVERSAL_IDENTIFIER` is the **canonical** prod id `e662fc1f-02c1-41ff-b8ba-c95a447b3965` (local bundle rewrites it to a throwaway that stays uncommitted). - New views reference app-owned fields only — no hardcoded system-field ids. ### Remaining before merge - CI lint / typecheck / tests (green locally). - Refresh the partners-doc (new objects/views change the app surface). --- ## 🚦 Release order — do not break ``` ① BRIEF WEB — #22291 ✅ MERGED (website deploy pending prod CLIENT_BRIEF_* env vars) │ ▼ ② GLOWUP APP — THIS PR (rk-partner-profile-page v1.3.0 → main) ⟵ replaces #22470 merge → DEPLOY TO PROD (verify canonical id first, yarn twenty deploy && install -r partner-twenty-com) → set new app variables on prod → refresh partners-doc │ ⟵⟵ GATE for ③ ⟵⟵ ▼ ③ GLOWUP WEB — rk-glowup-web-stacked (reopen ONE PR, base main; was #22471 / #22402) ONLY after ② is LIVE on prod (the site reads the new links / services / case-study objects) ``` - ② gates only ③. After ② deploys, reconcile **#22637** (partners-traffic-web) with ③ — both touch `partners-marketplace/*`. |
||
|
|
fa720358d9 |
Ignore call-recorder bots for unsupported meeting platforms (#23050)
## What The call-recorder scheduled a Recall bot for any calendar event that had a conference link, even when the link pointed to a platform Recall cannot join (e.g. ro.am, Daily, Whereby, or a plain dial-in). Those requests could never produce a recording. This adds a supported-platform check to the recording policy so unsupported links are ignored, with a dedicated reason, and documents the supported platforms in the app README. ## Changes - Add `SUPPORTED_MEETING_PLATFORM_URL_PATTERNS` constant (Zoom, Google Meet, Microsoft Teams, Webex, GoTo Meeting), extracted from the existing link-extraction patterns so extraction and validation share one source of truth. - Add `isSupportedMeetingPlatformUrl` util. - `resolveCallRecorderPolicyResult` now returns `UNSUPPORTED_MEETING_PLATFORM` (bot not required) when the resolved conference link is not a supported platform. - Document supported platforms and the ignore behavior in the call-recorder README. ## Tests - New unit tests for `isSupportedMeetingPlatformUrl`. - New policy test for the unsupported-platform case; updated existing policy tests to use real supported URLs. - All call-recorder unit tests pass; typecheck and lint clean. Closes twentyhq/core-team-issues#2705 --- _Generated by [Claude Code](https://claude.ai/code/session_01MWPkbdUg4QMdj4FM5mtNww)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23050?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. --> |
||
|
|
c90057178c |
Bump app version (#23049)
as title <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23049?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. --> |
||
|
|
89609c520c |
Reduce call-recorder Recall API load and harden bot scheduling recovery (#23014)
## Context We receive Recall rate limit alerts on `/api/v1/bot`. Recall's List Bots endpoint allows only 60 requests/min per Recall workspace (vs 300/min for Retrieve and 120/min for Create), and that budget is shared by every Twenty workspace on the instance since `RECALL_API_KEY` is a server-level variable. The call-recorder recovery crons fanned out one list call per stuck recording, fired at the same wall-clock minute for every workspace, and never resolved dead rows, so the pending set only grew. This PR reworks the bot-scheduling recovery mechanism so that crash-recovery work is rare, cheap, and mostly event-driven. One commit per change: ## Changes 1. **Fail never-scheduled recordings once their meeting ends** (`bot_never_scheduled` failure reason). Previously these rows stayed `REQUESTED+SCHEDULED` forever and were re-fetched by every recovery run. Rows with an unresolved creation attempt keep their recovery chance until the 7-day convergence lookback passes (a bot may have recorded before the id write-back was lost), then fail as `bot_schedule_outcome_unknown`. 2. **Batch bot lookups into one list call per run.** The pending-bot sweep and the failed-cancellation retry each issue at most one workspace-wide `GET /api/v1/bot/` (filtered by `twentyWorkspaceId` + active statuses) and match bots to recordings in memory via `twentyCallRecordingId` metadata, instead of one list call per stuck row. Truncated lists count as failed lookups so an incomplete map never authorizes a duplicate creation. 3. **Record a `botScheduleAttemptedAt` marker before POSTing a bot.** Recovery can now distinguish rows that never reached Recall (re-schedule directly, zero Recall reads) from rows whose creation outcome is unknown (only these join the lookup). 4. **Store the bot-creation `Idempotency-Key` on the row and recover by re-sending.** When a stuck row's stored key still hashes from the current scheduling inputs, recovery re-sends the creation: Recall either returns the existing bot or creates the intended one, all on the Create budget (120/min) without touching the List budget (60/min). Drifted inputs still fall back to the lookup. Re-sends preserve the first attempt's timestamp and are only trusted within a 12-hour window, so repeated unknown outcomes age into the lookup path rather than risking a twin bot after Recall's key retention expires. 5. **Resume pending rows on `callRecording.updated` events and slow the cron.** A new database-event trigger resumes scheduling within seconds when a row transitions back to pending (bot vanished at Recall, canceled request re-requested, failed row reset by reconciliation), with queue retries. It skips creations (the inserting run schedules inline), skips its own progress writes, uses slim-payload diffs to skip cheaply, and defers ambiguous rows to the cron so event bursts cannot fan out list calls. The pending-requests cron becomes a backstop and drops from every 5 minutes to every 15. Follow-up commits harden edge cases raised in review (status revalidation before POST, per-row cancellation recovery window, future-timestamp guard, attempt-state cleanup when a bot is confirmed gone at Recall) and add a lifecycle integration test. ## Notes - Two new app fields on `callRecording`: `botScheduleAttemptedAt` (DATE_TIME) and `botScheduleIdempotencyKey` (TEXT), both nullable and not UI-editable. - A tight race between the event trigger and the cron converges on one bot via the deterministic idempotency key. - Not addressed here (needs a server-side change): per-workspace jitter when dispatching logic-function cron triggers, so identical patterns don't fire for every workspace on the same minute. ## Test - New `call-recorder-lifecycle.integration-test.ts` on the app's integration harness: the global setup installs the app on a live test server, all reads and writes go through the real API into the test database, and only externals are mocked — the Recall API (a fetch interceptor that replays the same bot for a repeated `Idempotency-Key`, like the real API) and the trigger transports (webhook payloads invoke the webhook logic function handler; cron and database-event triggers run their flows). Thirteen scenarios assert the resulting CallRecording rows in the DB: scheduling from calendar reconciliation (events attached to a seeded `SHARE_EVERYTHING` calendar channel, since unassociated events are invisible), webhook status progression with artifact-import route calls, transcript completion, out-of-order delivery protection, fatal failure, unknown bots, cancellation with retried Recall delete, and every crash recovery path. Verified locally against a live server: 15 integration tests pass (including the existing schema contract test). - `yarn test:unit`: 488 tests pass. `yarn typecheck` and `yarn lint` clean. --------- Co-authored-by: martmull <martin@twenty.com> |
||
|
|
4e2f9e3416 |
Fix join at in the past (#23000)
as title, we floor the bot join at date 1second in the future <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23000?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. --> |
||
|
|
19f0e3cad5 |
Reduce Recall bot lifecycle reconciliation traffic (#22908)
## Summary - split pending Call Recording request maintenance from stale recording convergence - recover Recall bots by workspace and Call Recording metadata before creating a replacement - retry failed cancellations, including the canceled-plus-botless write-back race - move the broad orphaned-bot list sweep from every five minutes to a dedicated daily job - filter Recall bot lists by workspace metadata at the provider boundary ## Why This is stack 1/3 extracted from #22739. Healthy installed workspaces currently list Recall bots every five minutes even when no local state has diverged. This layer removes that unconditional list sweep while keeping pending request recovery at five-minute latency. The cancellation recovery also closes a crash window where Recall accepted a bot creation but the local bot ID write-back failed before the user canceled the request. The maintenance job now rediscovers and cancels that bot before it can join. ## Stack 1. **Recall bot lifecycle reconciliation** — this PR 2. Divergence-scoped recording synchronization — #22909 3. Artifact import offloading — #22910 ## Validation - `npm run typecheck` - `npm run lint` - `npm run test:unit` — 71 files, 454 tests --------- Co-authored-by: Claude <martmull@hotmail.fr> |
||
|
|
79f3a5243a |
Add callAppRoute to RestApiClient (#22863)
Adds a `callAppRoute` method to `RestApiClient` in `twenty-client-sdk/rest`. It calls one of the app's own HTTP routes using the injected `TWENTY_FUNCTIONS_URL`, resolved internally the same way the client already resolves `TWENTY_API_URL`, so app code no longer reads env vars or knows how function routes are hosted. Both app runtimes already go through `RestApiClient` for route calls (logic functions and front components), so both get this in one place; front components keep the existing 401 token-refresh flow. Pairs with #22825, which makes the injected `TWENTY_FUNCTIONS_URL` callable in every topology (app custom domain -> workspace isolated functions domain -> `SERVER_URL/s`). Once this ships in an SDK release, Call Recorder's own-route plumbing (logic-function and front-component utils) drops its URL resolution and calls `client.callAppRoute(path, body)`. --------- Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> |
||
|
|
cdb2590355 |
Migrate call-recorder tests off own-code vi.mock (#22902)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22902?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. --> |
||
|
|
2201917f33 |
Harden call recorder Recall API boundary (#22832)
Part 2/5 of splitting #22739. Stacked on #22831. - `RecallBotSnapshot`: Recall bot payloads are parsed once at the API boundary (`parseRecallBotSnapshot`); `getRecallBot`/`listScheduledRecallBots` return typed snapshots, flows never touch raw provider records - Retry policy: honors `Retry-After` (seconds or HTTP-date, capped at 60s), treats 409 and 507 (ad-hoc pool exhausted) as retryable with tailored delays, adds equal jitter to the linear backoff, and returns instead of sleeping past 10s in-process so invocations never sleep into their timeout - `listScheduledRecallBots` accepts a server-side `metadata__` filter and reports `truncated` instead of failing beyond 10 pages - Extracts `cancelOrEjectRecallBot` into the recall-api layer - Replaces the `CALL_RECORDER_MAX_MEDIA_FILE_SIZE_MB` server variable with a fixed 500 MB constant: uploads stream since #22652, so the cap no longer guards function memory and does not need to be operator-tunable Next: billing charge verification, divergence-scoped sync crons, webhook artifact continuation. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22832?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. --> |
||
|
|
f13bde1e03 |
Align call recorder vocabulary with core dialect (#22831)
Part 1/5 of splitting #22739 into a reviewable stack. Mechanical renames only, no behavior change: - `ingestion` -> `import` across data/domain/flows (`completeCallRecordingIngestion` -> `completeCallRecordingImport`, `ingestCallRecordingMedia` -> `importCallRecordingMedia`, `reconcileCallRecordingTranscriptArtifact` -> `importCallRecordingTranscript`, ...) - `reapOrphanedCallRecorders` -> `cleanupOrphanedRecallBots` - `ensureCallRecorder` -> `scheduleRecallBotForCallRecording`, `healCallRecordingsMissingBot` -> `scheduleRecallBotsForPendingCallRecordings` - `extractRecallBotConvergence` -> `extractRecallBotSyncState` - formatting drift in touched files Next in the stack: Recall API hardening, billing charge verification, divergence-scoped sync crons, webhook artifact continuation. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22831?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. --> |
||
|
|
bc1ccf526f |
chore(twenty-partners): upgrade to twenty-sdk 2.21 (#22869)
## What Upgrades the `twenty-partners` internal app to consume twenty-sdk 2.21. - Bump `twenty-sdk` and `twenty-client-sdk` from `2.19.0-alpha.1` to `2.21.0` (`package.json` + `yarn.lock`). - Replace the removed `generateDefaultFieldUniversalIdentifier` helper with `getFieldUniversalIdentifier` in `partner-applications.view.ts`, renaming the `fieldName` argument to `name`. ## Why `generateDefaultFieldUniversalIdentifier` was removed from the SDK and replaced by `getFieldUniversalIdentifier`. Both compute the same deterministic uuid-v5 (`fieldMetadata:objectUID:name` under the app universal identifier), so the resolved `createdAt` field identifier is unchanged; this is the only code change required to build against 2.21. ## Verified - Install resolves to 2.21.0; runtime check confirms `getFieldUniversalIdentifier` is exported and `generateDefaultFieldUniversalIdentifier` is gone. - Type check: no real errors. - `oxlint`: 0 warnings, 0 errors. --- _Generated by [Claude Code](https://claude.ai/code/session_01RUQ2yyfZcyKWqG57HbUYxx)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22869?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. --> |
||
|
|
a6af730353 |
chore: upgrade call-recorder, last-contact, people-data-labs to twenty-sdk 2.20 (#22852)
## What Upgrades three public apps to `twenty-sdk` 2.20. For each app, bumped `twenty-sdk` and `twenty-client-sdk` to `2.20.0`, raised the `engines.twenty` floor to `>=2.20.0`, and regenerated `yarn.lock`: - **call-recorder**: `2.19.0` -> `2.20.0` - **last-contact** (`@twentyhq/last-contact`): `2.19.0-alpha.1` -> `2.20.0` - **people-data-labs**: `2.19.0-alpha.1` -> `2.20.0` ## Verification - Lockfile diffs are version/checksum-only; the SDK's transitive dependency set is unchanged between 2.19 and 2.20, so no new packages were introduced. - `yarn typecheck` passes cleanly for all three apps against 2.20, confirming no breaking API changes to adapt to. --- _Generated by [Claude Code](https://claude.ai/code/session_01YWiC3qAbBcE1kvBMWvaxba)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22852?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. --> |
||
|
|
7f8a1da27a |
Fix Cloudflare rate limiting during last-contact backfill (#22811)
## Context
Upgrading `twenty-last-contact` on a production workspace failed with a
Cloudflare error 1015 ("You are being rate limited"). The
`backfill-last-contact` post-install function runs on every version
upgrade and fired 20 concurrent update mutations per batch with no pause
between batches, on top of paginated full-collection reads. On a
workspace with real email/calendar history that burst trips Cloudflare's
rate limit, and since the client SDK throws on any non-2xx response, a
single 429 killed the whole install/upgrade hook mid-backfill.
## Changes
- New `executeWithRetry` util: retries rate-limit (429 / Cloudflare
1015) and transient gateway/network errors (502/503/504, timeouts,
connection resets) with exponential backoff and jitter, capped at 5
attempts. Honors a `retry_after` hint when present in the response body.
Non-retryable errors still throw immediately.
- All backfill queries and mutations are wrapped with it.
- Update batch concurrency reduced from 20 to 10 to keep bursts under
the rate limit in the first place.
- Bumped app version to 1.1.1 with a changelog entry.
## Test
- Added unit tests for `executeWithRetry` (success passthrough,
retry-then-succeed, non-retryable passthrough, retry exhaustion,
`retry_after` handling).
- `yarn test:unit` (28 passed), `yarn typecheck`, `yarn lint` all green
in the app package.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01AtnkEfbpFhLp5qCJbSmZpE)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22811?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. -->
|
||
|
|
ace16add6c |
fix(twenty-partners): align createdAt view field with 2.19 deterministic ids and bump to 1.2.10 (#22782)
## What - Bump `twenty-partners` from `1.2.0` to `1.2.10`. - Fix the red integration tests by pointing the "Partner Applications" view `createdAt` column at the real field metadata id. ## Why the integration tests were red The `twenty-partners` CI job spins up `twentycrm/twenty-app-dev:latest` and runs the integration suite. The suite's global setup does a dev sync of the app, which failed: ``` Dev sync failed: viewField: INVALID_VIEW_DATA: Field metadata not found (universalIdentifier: 835c9a7e-72ec-46c5-8d90-39a02998f561) ``` The `partner-applications.view.ts` `createdAt` column referenced `PARTNER_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER = 421cbcea-...`, an invented id. `createdAt` is a reserved system field auto-created on the custom `partner` object, and since 2.19 its universal identifier is derived deterministically by the server from the application id, the object id and the field name. The invented id matched nothing, so the sync rejected the dangling view field and the app never registered, failing every integration test. ## Fix Set `PARTNER_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER` to the deterministically derived value `746e2944-28d0-545e-9832-a46516e1d9a0` (application id `e662fc1f-...` + partner object id `39101b39-...` + field name `createdAt`). This matches the same derivation the server uses for standard object system fields, verified against the `twenty-shared` opportunity `createdAt` snapshot. |
||
|
|
9e20e2222a |
Fix front-component serving on Safari, kill stale presigned caching, and cache built bundles client-side (#22672)
## Context Built front-component bundles are served via `GET /rest/front-components/:id/:cacheKey`. On S3-backed storage (Twenty Cloud) the endpoint used to 302-redirect the worker's authenticated fetch to a presigned S3 URL. That redirect caused two bugs, and fixing it removed the caching the redirect was accidentally providing — so this PR also adds a proper client-side cache. Closes twentyhq/core-team-issues#2653. ### Bug 1 — Safari 403 (Authorization header forwarded across redirect) The renderer worker fetches the bundle with `Authorization: Bearer`. The controller answered with a 302 to a presigned S3 URL. Per the Fetch spec, browsers must strip `Authorization` on a cross-origin redirect. Chrome/Firefox do, but Safari/WebKit forwards it, so S3 receives both a query-string signature and an `Authorization` header and rejects with `InvalidArgument: Only one auth mechanism allowed`. Result: front components never load in Safari on S3-backed storage. ### Bug 2 — 302 cached publicly (browser-independent) The redirect branch set no `Cache-Control`, so a CDN could cache it far beyond the presigned URL's TTL (`STORAGE_S3_PRESIGNED_URL_EXPIRES_IN`, 900s). Consequences: any client re-served the cached 302 after 15 min hits an expired signature (403, also affects Chrome), and the cached redirect containing a live presigned URL is served to unauthenticated requests (short-lived auth bypass). ### Regression this introduces — warm-load caching lost Marking the handoff `no-store` (Bug 2 fix) is correct, but it means the built bundle is no longer cached anywhere on the S3 path. The browser HTTP cache cannot compensate: the presigned URL that actually returns the bytes carries a fresh `X-Amz-Date`/`X-Amz-Signature` on every request, so each download is a brand-new cache key and never hits. Net effect without mitigation: every worker mount re-downloads the full bundle. ## What changed - **Front components return a 200 JSON body instead of a 302.** The controller now responds `200 { url }` with `Cache-Control: private, no-store`. The worker parses the JSON and issues a separate header-less `fetch(url)` to S3. No redirect means the `Authorization` header is never forwarded, making it browser-independent, and the handoff carrying the presigned URL is never cached. The stream path (local storage) is unchanged. - **Client-side bundle cache in the renderer (restores warm loads).** `fetchComponentSource` wraps the fetch chain in a `CacheStorage` layer keyed by the **content-addressed** `/front-components/:id/:checksum.js` URL. A hit returns the stored bundle and skips **both** the `no-store` handoff to Twenty and the S3 download — restoring cross-session warm loads without ever persisting a presigned credential. Because `CacheStorage` is writable by any same-origin code (including the untrusted component code this cache feeds), cached content is verified against the sha-256 checksum embedded in the URL on every read, and evicted on mismatch. Caching degrades to a plain fetch where `CacheStorage` or WebCrypto is unavailable. - **sha-256 checksums for built front components.** The SDK build and workspace prefill now fingerprint built front-component bundles with sha-256 (WebCrypto has no md5), enabling the integrity check above. Other file folders keep md5. Legacy md5-fingerprinted URLs (32-hex) simply bypass the cache — already-synced components keep working and start benefiting from caching on their next build/sync. - **WebKit e2e coverage.** Added a `webkit` project to the postcard example's Playwright config mirroring `chrome` (shared setup + storageState), plus iframe/worker diagnostics logging so front-component failures surface in the test log. `TZ` is pinned to `Europe/Paris` because WebKit on Linux ignores Playwright's `timezoneId` emulation and rejects the runner's legacy `CET` alias, which crashed the record page before the component could render. ### Why we hand off to S3 instead of streaming through Twenty On S3-backed storage we deliberately **do not** proxy/stream the bundle bytes through the API. The controller returns the presigned URL and the worker fetches the content directly from S3, for two reasons: - **Server CPU/bandwidth.** Streaming every bundle on every cold load would put the API server on the hot path for all front-component content. Handing off to S3 keeps that load off the server. - **Domain isolation.** Front-component content is fetched from the object-storage domain (e.g. `s3.domain.com`), a different origin than the API and the front app. Serving untrusted/app-authored bundle content from a separate domain than `twenty.com` keeps it off the app's origin. The stream path is kept only as the local-storage fallback (no S3/presign available), where these concerns don't apply. ## Examples ### The JSON handoff (S3 path) ```http GET /rest/front-components/d3b07384-.../a1b2c3d4.js HTTP/1.1 Host: twenty.com Authorization: Bearer <worker-token> ``` ```http HTTP/1.1 200 OK Content-Type: application/json Cache-Control: private, no-store {"url":"https://s3.domain.com/bucket/.../checkout-widget.mjs?X-Amz-Date=20260709T091500Z&X-Amz-Expires=900&...&X-Amz-Signature=AAAA1111..."} ``` The worker then fetches that presigned URL **without** headers (the Safari fix) and gets the bundle bytes. ### Why the browser HTTP cache can't reuse it | | Load 1 (09:15) | Load 2 (09:30) | Same key? | |---|---|---|---| | Twenty handoff URL | `.../a1b2c3d4.js` | `.../a1b2c3d4.js` | ✅ but response is `no-store` | | Presigned `X-Amz-Signature` | `AAAA1111...` | `ZZZZ9999...` | ❌ | | Effective S3 URL (the HTTP cache key) | `...&X-Amz-Signature=AAAA1111...` | `...&X-Amz-Signature=ZZZZ9999...` | ❌ new key → miss | ### What the CacheStorage layer stores ``` key = https://twenty.com/rest/front-components/d3b07384-.../a1b2c3d4.js (stable, chosen by us) value = <bundle JS bytes> (NOT the presigned URL) ``` Keying by the stable logical URL (not the volatile URL the bytes arrived from) is the one thing the native HTTP cache can't express. The presigned URL is used once and discarded. ### Invalidation No TTL and no explicit delete — invalidation is by key change. A rebuild changes the checksum → changes the URL → guaranteed miss on the new key. The old entry is orphaned and reclaimed by normal browser eviction (quota/LRU; Safari ITP after 7 idle days). Global invalidation lever: bump the cache name suffix (`front-component-source-v1`). ## Deploy note — front/server release window Old frontend bundles (already-open tabs) hitting the new server receive the JSON handoff where they expect raw JS and fail to render until the tab is reloaded. The other direction is safe: the new worker against an old server follows the 302 transparently (the content-type check falls through to `response.text()`). Accepted as a short deploy-window trade-off. ## Follow-ups (not in this PR) - The client-side cache is a bridge for the `no-store` presigned handoff. If built components are later served from a stable, non-signed, public-by-URL path (they are already content-addressed by checksum, so `immutable` is safe), the browser + CDN cache natively and this custom layer can be removed. - `GET /file/:fileFolder/:id` presigned 302s still carry no `Cache-Control`. An explicit policy there (bounded `private, max-age` below the presigned TTL) was prototyped in this PR and deliberately dropped to keep the scope on front components — the file path authenticates via a query-param token (part of any cache key), so its exposure differs and deserves its own PR. ## Non-goals Per the issue, file serving keeps its query-param token + 302 model. Native browser loads (`<img>`, downloads) cannot do a two-step fetch and already work on Safari. The public-asset redirect is left untouched since its caching is intentional. ## Test plan - Renderer: `fetchComponentSource.spec.ts` covers cache miss + write, verified cache hit (no network), poisoned-entry eviction, checksum-mismatch (never cached), non-fingerprinted and legacy-md5 URL bypass, and the no-`CacheStorage` / no-WebCrypto fallbacks. `fetchComponentSourceFromNetwork.spec.ts` covers the direct JS response, the JSON handoff follow-through (header-less presigned fetch), and error mapping. - e2e: the postcard front-component spec now runs on both Chromium and WebKit against prod-parity storage (S3 + Lambda). - `oxlint` + `oxfmt` clean; typecheck passes on changed packages. ### Reproduction proof — Safari was always broken (e2e probe) We ran the prod-parity postcard e2e suite (S3 storage + Lambda) with WebKit against **`main` without this fix**, via a throwaway probe PR: twentyhq/twenty#22717. Result — [ci-privileged run 29015624468](https://github.com/twentyhq/ci-privileged/actions/runs/29015624468): ``` 1 failed [webkit] › card-front-component.spec.ts:61 › renders the postcard name and status badge in the record preview 2 passed (1.4m) ``` `[webkit]` times out waiting for `getByTestId('postcard-card')` to become visible (*element(s) not found*) while the Chromium run of the same spec passes. This confirms the front component **never rendered in Safari** on S3-backed storage prior to this PR — it is a genuine, browser-specific bug, not a flake. The fix in this PR is expected to turn that same `[webkit]` assertion green. Note: running the WebKit tests in CI requires the WebKit browser binary and its system dependencies in the e2e job (now installed via `npx playwright install --with-deps chromium webkit`). |
||
|
|
23cae2040a |
Improve application asset management (#22564)
App manifests could point the logo and screenshots at either external
URLs or public folder paths, and that was handled inconsistently across
install, sync and the marketplace.
This makes assets always bundled files:
- Manifests now use `logo` and `galleryImages` (a `string[]` of public
folder paths) instead of `logoUrl` and `screenshots`. The old fields
still work but are deprecated. Gallery order comes from the array index.
Normalization (deprecated-field migration, and warning about + ignoring
external URLs) happens in `defineApplication`, so the warnings surface
at define time.
- Logo is stored as a File record (`logoFileId`).
- The registration gallery is configured via a `settings` jsonb column
on `applicationRegistration` (`{ galleryImages: string[] }`) — populated
from the manifest, read by the marketplace detail (falling back to the
legacy `screenshots` column, then the manifest). No dedicated gallery
table.
- The marketplace detail DTO and front now use `galleryImages`.
Verified against a local Postgres: the fast instance commands run with
no pending-migration diff, the schema is correct, and the server boots.
Typecheck, lint, codegen and the application unit tests pass.
Not included yet: rehosting assets into storage for npm catalog and
tarball registrations, versioned cache busting on the serving route, and
a backfill for existing installs.
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22564?utm_source=github"
rel="nofollow noreferrer noopener" target="_blank">``<img alt="Review
in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg">``</a>
|
||
|
|
d8082d86ca |
Add Last contact on Companies and Opportunities to last-contact app (#22720)
## After Example with google company, 2 contacts, 2 opportunities: <img width="1512" height="332" alt="image" src="https://github.com/user-attachments/assets/9dac3ac1-bbbb-41a4-b6d6-5f2180e33c13"/> <img width="1512" height="313" alt="image" src="https://github.com/user-attachments/assets/e6e326db-942f-45c2-a696-05fa4c32619b"/> <img width="1309" height="313" alt="image" src="https://github.com/user-attachments/assets/64ed84ab-f7f0-47e5-b48c-42572c7d2534"/> ## What Extends the `twenty-last-contact` app so **Last contact** is also surfaced on **Companies** and **Opportunities**, not just People. Feedback: Companies and Opportunities already show emails and meetings from their related Person records on their timeline, so they should expose the most recent touch as fields too. Terminology and mechanism intentionally mirror what the app already does on People (no email-specific fields). ## Changes - **New fields**, identical in name/label/semantics to the People headline columns: - `Company.lastContactAt` and `Opportunity.lastContactAt` (datetime, "Last contact") - `Company.lastContactItemMessage` / `lastContactItemCalendarEvent` and the same pair on Opportunity (morph relation, "Last contact item"), with inverse `lastContactForCompanies` / `lastContactForOpportunities` relations on Message and Calendar event - All app fields are read-only in the UI (`isUIEditable: false`) - **View fields** on the All Companies and All Opportunities views (Last contact + Last contact item, visible). - **Live updates**: the new `updateRelatedLastContact` util propagates a person's interaction to their company and their point-of-contact opportunities, guarded so an older interaction never overwrites a newer one. It is called from both the email handler (`on-email-interaction`) and the shared calendar path (`updatePersonLastContactFromCalendar`, used by `on-calendar-interaction` and `on-calendar-event-started`), so emails and meetings both count. - **Backfill** (`backfill-last-contact`): aggregates each person's last contact up to their company, and each opportunity's from its point of contact. Related-record scope: a company's last contact comes from its people; an opportunity's from its point of contact. ## Not included (deliberately) The directional fields (`lastInboundAt`, `lastOutboundAt`, `lastContactBy`) and the `lastEmail`/`lastMeeting` shortcuts are not mirrored: aggregated across many people they get semantically fuzzy, and they would double the write amplification on every synced email for little added signal. ## Tests - Unit tests for `updateRelatedLastContact` (email and meeting propagation, recency guard, no-company case). - Integration tests: a related person's email sets company + opportunity last contact; a later meeting supersedes an email; an older interaction does not overwrite a newer one. - Existing unit + integration tests updated and passing; typecheck and lint clean. |
||
|
|
78a0f9ea77 | feat(call-recorder): type application and server variables, make summary prompt rich text (#22685) | ||
|
|
545de99476 |
[Twenty Fireflies] Add calendar event summary and transcript UI components (#22667)
## Summary Add comprehensive UI components and hooks for displaying call recording summaries and transcripts on calendar event pages. This includes markdown parsing for summaries, diarized transcript rendering, and data fetching hooks integrated with the Twenty SDK. ## Key Changes ### New Hooks - `useCalendarEventSummary`: Fetches and manages summary markdown for a calendar event's call recordings - `useCalendarEventTranscript`: Fetches and manages transcript data for a calendar event's call recordings ### Summary Components - `CalendarEventSummary`: Top-level component that displays summary for selected calendar event - `CalendarEventSummaryContent`: Container with header and content frame - `CalendarEventSummaryBody`: Handles loading, error, and empty states - `SummaryMarkdown`: Renders parsed markdown with support for headings, lists, and paragraphs - `SummaryInlineSegments`: Renders inline text with bold formatting support ### Transcript Components - `CalendarEventTranscript`: Top-level component that displays transcript for selected calendar event - `CalendarEventTranscriptContent`: Container with header and scrollable content frame - `CalendarEventTranscriptBody`: Handles loading, error, and empty states - `TranscriptEntryList`: Renders list of transcript entries - `TranscriptEntryListItem`: Individual transcript entry with speaker avatar, timestamp, and text - `TranscriptErrorBox`: Styled error state display ### Utilities - `parseSummaryMarkdownBlocks`: Parses markdown into structured blocks (headings, lists, paragraphs) - `parseSummaryInlineSegments`: Parses inline markdown for bold text formatting - `parseTranscriptEntries`: Parses diarized transcript format into structured entries with speaker info and timestamps - `formatSecondsAsClockTimestamp`: Formats seconds into HH:MM:SS or MM:SS format - `asRecord`: Type guard utility for converting values to records ### Page Layout Configuration - `calendar-event-summary-tab.ts`: Defines Summary tab for calendar event page layout - `calendar-event-transcript-tab.ts`: Defines Transcript tab for calendar event page layout - Front component definitions for both summary and transcript ### Types - `TranscriptEntry` and `TranscriptWord`: Structured transcript data types - `SummaryMarkdownBlock`: Block-level markdown structure - `SummaryInlineSegment`: Inline text segment with formatting ## Implementation Details - Uses `CoreApiClient` from Twenty SDK to query call recordings filtered by calendar event ID - Implements proper cleanup with cancellation tokens to prevent state updates on unmounted components - Supports both loading and error states with user-friendly messaging - Markdown parser handles headings (h1-h6), bullet lists, and paragraphs with inline bold formatting - Transcript parser validates diarized format and gracefully handles malformed entries - Styled with emotion and theme constants from twenty-ui for consistent design - Integrates with `useSelectedRecordIds` hook to track selected calendar event https://github.com/user-attachments/assets/0bd63590-bb1e-4f89-8b96-e3fbb659473e <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22667?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. --> |
||
|
|
d746909184 |
feat(sdk): validate graph page-layout widgets at build time (#22559)
When an app defines a graph widget (aggregate, pie, bar or line chart), the built manifest can carry the wrong key and the server rejects it at sync time with a confusing "aggregate field is required" error. The SDK type already requires `aggregateFieldMetadataUniversalIdentifier` and renames the raw `aggregateFieldMetadataId` at compile time. But the manifest build runs esbuild with no type checking, so a wrong or missing key slips through and only fails later on the server. This adds a build-time check that mirrors the server validator, with a hint pointing at the right key when the raw one was used. It is non-breaking since correctly authored apps already use the universal key. Tests: unit tests on the validator, plus a real graph widget added to the rich-app fixture so the integration and e2e suites cover the happy path. |
||
|
|
640e6b8b33 |
fix(call-recorder): stream Recall media to storage to fix OOM (#22652)
## Summary Fixes Call Recorder media ingestion OOMs by streaming Recall media into Twenty direct uploads instead of buffering the full file in memory. ## Changes - Opens the Recall media download stream and uses its `Content-Length` as the direct upload size. - Creates a Twenty direct upload target, streams the media body to it with Node `http`/`https` backpressure, then completes the upload. - Cleans up download/upload streams on target creation, upload, and storage response failures. - Keeps the media size cap for now while making it no longer required for memory safety. - Bumps `twenty-client-sdk` and `twenty-sdk` to `2.19.0`. ## Tests - `yarn test:unit src/logic-functions/flows/__tests__/ingest-call-recording-media.test.ts src/logic-functions/flows/__tests__/put-media-download-body-to-upload-target.test.ts` - `yarn typecheck` |